๐Ÿ”ฅ Oxide UI v0.1.0 โ€” Dark mode only, copy-paste ready. Get started โ†’
Skip to content

Tag Input โ€‹

<input> + tag list

A text field that builds a list of short tokens. Users press Enter or , to add a tag, Backspace to remove the last one. Optionally supports suggestion chips.

Overview โ€‹

Preview
nuxt tailwind

Press Enter or , to add a tag

Vue 3

Basic tag input โ€‹

vue
<script setup>
import { ref } from 'vue'

const inputEl = ref(null)
const value = ref('')
const focused = ref(false)
const tags = ref(['nuxt', 'tailwind'])

function add() {
  const v = value.value.trim().replace(/,$/, '')
  if (v && !tags.value.includes(v)) tags.value.push(v)
  value.value = ''
}

function remove(tag) {
  tags.value = tags.value.filter(t => t !== tag)
}

function onBackspace() {
  // Delete last tag when input is empty
  if (!value.value && tags.value.length) tags.value.pop()
}
</script>

<template>
  <div
    @click="inputEl.focus()"
    class="flex min-h-10 flex-wrap items-center gap-1.5 rounded-md border bg-zinc-900 px-2.5 py-2 cursor-text transition-colors"
    :class="focused ? 'border-[#D40C37] ring-2 ring-[#D40C37]/15' : 'border-zinc-700'"
  >
    <!-- Tags -->
    <span
      v-for="tag in tags"
      :key="tag"
      class="inline-flex items-center gap-1 rounded-md bg-zinc-800 pl-2 pr-1 py-0.5
             text-xs font-medium text-zinc-200 ring-1 ring-inset ring-zinc-700/50"
    >
      {{ tag }}
      <button
        @click.stop="remove(tag)"
        class="flex h-3.5 w-3.5 items-center justify-center rounded text-zinc-500
               hover:bg-zinc-700 hover:text-zinc-200 transition-colors"
        :aria-label="`Remove ${tag}`"
      >โœ•</button>
    </span>

    <!-- Input -->
    <input
      ref="inputEl"
      v-model="value"
      type="text"
      :placeholder="tags.length === 0 ? 'Add tags...' : ''"
      class="flex-1 min-w-16 bg-transparent text-sm text-zinc-100 placeholder-zinc-600 outline-none"
      @focus="focused = true"
      @blur="focused = false"
      @keydown.enter.prevent="add"
      @keydown.comma.prevent="add"
      @keydown.backspace="onBackspace"
    />
  </div>
  <p class="mt-1.5 text-xs text-zinc-600">
    Press <kbd>Enter</kbd> or <kbd>,</kbd> to add ยท <kbd>Backspace</kbd> to remove last
  </p>
</template>

Accent variant (brand colour tags) โ€‹

html
<span class="inline-flex items-center gap-1 rounded-md bg-[#D40C37]/10 pl-2 pr-1 py-0.5
             text-xs font-medium text-[#D40C37] ring-1 ring-inset ring-[#D40C37]/20">
  Vue 3
  <button class="... text-[#D40C37]/60 hover:text-[#D40C37]">โœ•</button>
</span>

With suggestion chips โ€‹

vue
<script setup>
const suggestions = ['Nuxt', 'React', 'TypeScript', 'Tailwind CSS', 'Pinia']

const filteredSuggestions = computed(() =>
  suggestions.filter(s =>
    !tags.value.includes(s) &&
    s.toLowerCase().includes(value.value.toLowerCase())
  )
)

function addSuggestion(s) {
  if (s && !tags.value.includes(s)) {
    tags.value.push(s)
    value.value = ''
  }
}
</script>

<template>
  <!-- tag input field above -->

  <!-- Suggestion chips appear below the input when focused -->
  <div v-if="focused && filteredSuggestions.length" class="flex flex-wrap gap-1.5 pt-2">
    <button
      v-for="s in filteredSuggestions"
      :key="s"
      @mousedown.prevent="addSuggestion(s)"
      class="rounded-md border border-zinc-700 px-2 py-0.5 text-xs text-zinc-400
             hover:border-zinc-500 hover:text-zinc-200 transition-colors"
    >
      + {{ s }}
    </button>
  </div>
</template>

With max tag limit โ€‹

vue
<script setup>
const MAX_TAGS = 5

function add() {
  if (tags.value.length >= MAX_TAGS) return
  const v = value.value.trim()
  if (v && !tags.value.includes(v)) tags.value.push(v)
  value.value = ''
}
</script>

<template>
  <!-- Disable input when limit reached -->
  <input
    v-model="value"
    :disabled="tags.length >= MAX_TAGS"
    :placeholder="tags.length >= MAX_TAGS ? `Max ${MAX_TAGS} tags` : 'Add a tag...'"
    ...
  />
</template>

Accessibility notes โ€‹

  • Each remove button needs a unique aria-label โ€” e.g. "Remove nuxt" โ€” not just "โœ•"
  • The container <div> that wraps the tags and input should have role="group" with an aria-label like "Tags"
  • Announce tag additions and removals with a role="status" live region
  • Use @mousedown.prevent on suggestion chips to prevent the input from losing focus before the click registers

Released under the MIT License.