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 haverole="group"with anaria-labellike"Tags" - Announce tag additions and removals with a
role="status"live region - Use
@mousedown.preventon suggestion chips to prevent the input from losing focus before the click registers