Tags Input
Overview
A composable multi-value input for adding, displaying, and removing tags. Supports controlled/uncontrolled string[] values, tag creation via Enter/Tab/blur/delimiter typing/paste, full keyboard selection and removal (with RTL reversal), max/duplicate constraints, and hidden form-input serialization. Use it for keyword lists, email recipients, or any small-token collection that needs fast keyboard workflows instead of a plain text input.
Usage
Features
- 🏷 Tag model — controlled/uncontrolled
string[]viauseControllableState; add/remove/clear events plus per-actionaddTag/removeTagemits - ⌨️ Full keyboard support —
ArrowLeft/ArrowRightselect the adjacent tag (logical direction, reversed in RTL),Backspace/Deleteremove the selected tag,Home/Endjump to the first/last tag, typing deselects - ✂️ Delimiter & paste — commit the typed value when a delimiter (string or
RegExp) is entered, or split pasted text (addOnPaste) - ⏎ Commit triggers —
Enteralways commits;Taband blur commit only withaddOnTab/addOnBlur - 🚦 Constraints —
maxcaps the total count,duplicateallows repeated values,displayValuerewrites the label; rejected values emitinvalid - 🧩 Composable parts —
Root/Control/Item/ItemText/ItemDelete/Clearwith*Propspassthrough on the compact and a fully replaceableitemslot - 📋 Form integration —
name/required/disabledserialize through a visually hidden input;aria-controlslinks the input to the tag list container - 🔤 Localized UI — default
aria-labels come from locale messages and can be overridden per part - 🚫 Disabled / readonly —
disabledblocks all interaction;readonlyblocks typing but keeps tag removal and clearing available
Component family
STagsInput— the styled wrapper that forwards props to the Aria compact and injectstagsInputVariantsclasses (6 slots: root/item/itemText/itemDelete/control/clear)TagsInputCompact(Aria) — data-driven composition ofTagsInputRoot+ oneTagsInputItemper value (default item text/delete) +Control+Clear(gated byclearable); import from@vean/aria/tags-inputfor unstyled usageTagsInputRoot(Aria) — state owner:useControllableStatefor the value array, collection registration, keydown/removal/clear logic, and the visually hidden form inputTagsInputControl(Aria) — the committed input: delimiter typing, paste splitting,Enter/Tab/blur commit, composition guardTagsInputItem/TagsInputItemText/TagsInputItemDelete(Aria) — a registered collection item with its label span and keyboard-reachable delete buttonTagsInputClear(Aria) — the clear-all button, hidden until the root has a value (styling) and gated byclearable(rendering)
Demos
Basic
API
Aria Layer
8TagsInput
Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | ClassValue | — | Additional class names applied to the root element. |
size | ThemeSize | — | Visual size of the component. |
ui | Partial<TagsInputUi> | — | Per-slot class overrides for the component. |
clearable | boolean | — | Whether to render the clear trigger. |
controlProps | TagsInputControlProps | — | Props forwarded to the input element. |
clearProps | TagsInputClearProps | — | Props forwarded to the clear element. |
itemProps | TagsInputItemProps | — | Props forwarded to the item element. |
itemTextProps | TagsInputItemTextProps | — | Props forwarded to the item text element. |
itemDeleteProps | TagsInputItemDeleteProps | — | Props forwarded to the item delete element. |
dir | Direction | — | The reading direction of the tags input. |
modelValue | string[] | — | The controlled value of the tags input. |
defaultValue | string[] | — | The uncontrolled default value of the tags input. |
addOnPaste | boolean | — | Whether to add tags on paste. |
addOnTab | boolean | — | Whether to add tags on tab. |
addOnBlur | boolean | — | Whether to add tags on blur. |
duplicate | boolean | — | Whether to allow duplicated tags. |
delimiter | string | RegExp | — | The delimiter used to add tags. |
max | number | — | Maximum number of tags. Set to 0 for unlimited. |
displayValue | ((value: string) => string) | "(value: string) => value" | Display the value of the tag. Useful when you want to apply modifications to the value like adding a suffix |
id | string | — | Id of the input element |
autofocus | boolean | — | When `true`, the input is auto-focused. |
autocomplete | 'search' | 'name' | 'email' | 'tel' | 'url' | 'on' | 'off' | 'username' | 'current-password' | 'new-password' | 'one-time-code' | 'given-name' | 'family-name' | 'organization' | 'street-address' | 'address-level2' | 'postal-code' | 'country' | 'cc-name' | 'cc-number' | 'cc-exp' | 'cc-csc' | 'transaction-amount' | — | The autocomplete attribute of the input.
Supports the HTML autofill tokens (including password-manager values such as
`current-password` / `new-password`), in addition to the `on` / `off` switches. |
disabled | boolean | — | When `true`, prevents the user from interacting with the input. |
maxlength | number | — | The maximum number of characters allowed in the input |
minlength | number | — | The minimum number of characters allowed in the input |
pattern | string | — | The pattern attribute of the input |
placeholder | string | — | The placeholder of the input |
readonly | boolean | — | When `true`, the input is read-only. |
type | InputTypeHTMLAttribute | — | The type of the input element. |
name | string | — | The name of the field. Submitted with its owning form as part of a name/value pair. |
required | boolean | — | When `true`, indicates that the user must set the value before the owning form can be submitted. |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | string[] | Emitted when the model value changes. |
invalid | string | Emitted when invalid occurs. |
addTag | string | Emitted when add tag occurs. |
removeTag | string | Emitted when remove tag occurs. |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
item | TagsInputCompactSlotProps | Custom item content or full item replacement. |
TagsInputClear
Props
| Prop | Type | Default | Description |
|---|---|---|---|
type | ButtonType | 'button' | The type of the button element. Can be one of 'button', 'submit', or 'reset'. |
disabled | boolean | — | Whether the component is disabled. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
TagsInputCompact
Props
| Prop | Type | Default | Description |
|---|---|---|---|
clearable | boolean | — | Whether to render the clear trigger. |
controlProps | TagsInputControlProps | — | Props forwarded to the input element. |
clearProps | TagsInputClearProps | — | Props forwarded to the clear element. |
itemProps | TagsInputItemProps | — | Props forwarded to the item element. |
itemTextProps | TagsInputItemTextProps | — | Props forwarded to the item text element. |
itemDeleteProps | TagsInputItemDeleteProps | — | Props forwarded to the item delete element. |
dir | Direction | — | The reading direction of the tags input. |
modelValue | string[] | — | The controlled value of the tags input. |
defaultValue | string[] | — | The uncontrolled default value of the tags input. |
addOnPaste | boolean | — | Whether to add tags on paste. |
addOnTab | boolean | — | Whether to add tags on tab. |
addOnBlur | boolean | — | Whether to add tags on blur. |
duplicate | boolean | — | Whether to allow duplicated tags. |
delimiter | string | RegExp | — | The delimiter used to add tags. |
max | number | — | Maximum number of tags. Set to 0 for unlimited. |
displayValue | ((value: string) => string) | "(value: string) => value" | Display the value of the tag. Useful when you want to apply modifications to the value like adding a suffix |
id | string | — | Id of the input element |
autofocus | boolean | — | When `true`, the input is auto-focused. |
autocomplete | 'search' | 'name' | 'email' | 'tel' | 'url' | 'on' | 'off' | 'username' | 'current-password' | 'new-password' | 'one-time-code' | 'given-name' | 'family-name' | 'organization' | 'street-address' | 'address-level2' | 'postal-code' | 'country' | 'cc-name' | 'cc-number' | 'cc-exp' | 'cc-csc' | 'transaction-amount' | — | The autocomplete attribute of the input.
Supports the HTML autofill tokens (including password-manager values such as
`current-password` / `new-password`), in addition to the `on` / `off` switches. |
disabled | boolean | — | When `true`, prevents the user from interacting with the input. |
maxlength | number | — | The maximum number of characters allowed in the input |
minlength | number | — | The minimum number of characters allowed in the input |
pattern | string | — | The pattern attribute of the input |
placeholder | string | — | The placeholder of the input |
readonly | boolean | — | When `true`, the input is read-only. |
type | InputTypeHTMLAttribute | — | The type of the input element. |
name | string | — | The name of the field. Submitted with its owning form as part of a name/value pair. |
required | boolean | — | When `true`, indicates that the user must set the value before the owning form can be submitted. |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | string[] | Emitted when the model value changes. |
invalid | string | Emitted when invalid occurs. |
addTag | string | Emitted when add tag occurs. |
removeTag | string | Emitted when remove tag occurs. |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
item | TagsInputCompactSlotProps | Custom item content or full item replacement. |
TagsInputControl
TagsInputItem
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Value associated with the tag. |
disabled | boolean | — | Whether to disable the tag item. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
TagsInputItemDelete
Props
| Prop | Type | Default | Description |
|---|---|---|---|
type | ButtonType | 'button' | The type of the button element. Can be one of 'button', 'submit', or 'reset'. |
disabled | boolean | — | Whether the component is disabled. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
TagsInputItemText
Props
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
TagsInputRoot
Props
| Prop | Type | Default | Description |
|---|---|---|---|
dir | Direction | — | The reading direction of the tags input. |
modelValue | string[] | — | The controlled value of the tags input. |
defaultValue | string[] | — | The uncontrolled default value of the tags input. |
addOnPaste | boolean | — | Whether to add tags on paste. |
addOnTab | boolean | — | Whether to add tags on tab. |
addOnBlur | boolean | — | Whether to add tags on blur. |
duplicate | boolean | — | Whether to allow duplicated tags. |
delimiter | string | RegExp | — | The delimiter used to add tags. |
max | number | — | Maximum number of tags. Set to 0 for unlimited. |
displayValue | ((value: string) => string) | "(value: string) => value" | Display the value of the tag. Useful when you want to apply modifications to the value like adding a suffix |
id | string | — | Id of the input element |
autofocus | boolean | — | When `true`, the input is auto-focused. |
autocomplete | 'search' | 'name' | 'email' | 'tel' | 'url' | 'on' | 'off' | 'username' | 'current-password' | 'new-password' | 'one-time-code' | 'given-name' | 'family-name' | 'organization' | 'street-address' | 'address-level2' | 'postal-code' | 'country' | 'cc-name' | 'cc-number' | 'cc-exp' | 'cc-csc' | 'transaction-amount' | — | The autocomplete attribute of the input.
Supports the HTML autofill tokens (including password-manager values such as
`current-password` / `new-password`), in addition to the `on` / `off` switches. |
disabled | boolean | — | When `true`, prevents the user from interacting with the input. |
maxlength | number | — | The maximum number of characters allowed in the input |
minlength | number | — | The minimum number of characters allowed in the input |
pattern | string | — | The pattern attribute of the input |
placeholder | string | — | The placeholder of the input |
readonly | boolean | — | When `true`, the input is read-only. |
type | InputTypeHTMLAttribute | — | The type of the input element. |
name | string | — | The name of the field. Submitted with its owning form as part of a name/value pair. |
required | boolean | — | When `true`, indicates that the user must set the value before the owning form can be submitted. |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | string[] | Emitted when the model value changes. |
invalid | string | Emitted when invalid occurs. |
addTag | string | Emitted when add tag occurs. |
removeTag | string | Emitted when remove tag occurs. |
Notes
Architecture and benchmark differences
TagsInputRoot owns the value via useControllableState and the collection registry (each Item registers through useCollectionItem). TagsInputControl only commits values: it watches delimiter/paste/Enter/Tab/blur, trims and validates through the root’s onAddValue, and reports rejections via the invalid event. Keyboard selection state (selectedElement) lives on the root so the control’s keydown handler can move selection with arrow keys (logical direction, RTL-aware) and remove tags with Backspace/Delete/Home/End. The compact iterates the value array with an index-value key so duplicated values stay stable without in-place value patching. Most benchmark libraries expose tags as a mode of a single select; the Aria/styled split, per-part *Props passthrough, slot-driven item rendering, and the invalid/addTag/removeTag event surface are the differentiators.
| Capability | VeanUI | Ant Design | Element Plus | Mantine | Naive UI | shadcn |
|---|---|---|---|---|---|---|
| Aria/styled split | ✅ | — | — | — | — | — |
| Controlled / uncontrolled | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Enter / delimiter / paste add | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Keyboard tag selection + removal | ✅ | — | — | ✅ | — | — |
| RTL direction reversal | ✅ | — | — | — | — | — |
max / duplicate constraints | ✅ | ✅ | ✅ | ✅ | ✅ | — |
RegExp delimiter | ✅ | — | — | — | — | — |
Per-part *Props passthrough | ✅ | — | — | — | — | — |
| Custom item slot | ✅ | ✅ | ✅ | ✅ | — | ✅ |
| Form hidden-input serialization | ✅ | — | — | — | — | — |
Localized aria-labels | ✅ | ✅ | ✅ | ✅ | ✅ | — |
Cautions
- The value is always a
string[]; duplicates are rejected unlessduplicateis set.displayValuechanges the label only — the stored value stays the raw string. max={0}means unlimited; any positive number caps the count and rejected additions emitinvalidwith the attempted value.addOnBlurcommits pending text on blur, but a blur that lands on a tag’s delete/clear button inside the tag list is skipped (guarded viaaria-controls).addOnTabprevents the default tab-out and commits instead; if you need to leave the field, disable it or commit on blur.- The compact renders the clear trigger whenever
clearableis notfalse; the default styles hide it until the root has a value. - In controlled mode (
modelValuebound), the parent must update the bound array — the component only emitsupdate:modelValue. readonlykeeps the input read-only but still allows tag removal and clearing; usedisabledto block all interaction.- The input’s
aria-controlspoints at the derived tag-list container id ({id}-tags-list); pass anidto enable the blur guard and form-label association.
FAQ
How do I add tags on Enter only?
That’s the default — type a value and press Enter. Tab/blur commit only when addOnTab/addOnBlur are enabled.
How do I let users paste multiple tags at once?
Set addOnPaste and keep the default delimiter="," — pasting "a,b,c" creates three tags. Use a RegExp delimiter for flexible splits, e.g. :delimiter="/[,,]/".
How do I prevent duplicate tags?
Duplicates are rejected by default and emit invalid with the rejected value. Pass duplicate to allow repeats (the compact keys each item by its position so repeats stay stable).
How does the value reach a form submission?
With name set, the values serialize into a visually hidden input (one per tag). Combined with required, an empty tag list marks the control invalid.
Why did my controlled value not update?
In controlled mode the component only emits update:modelValue; you must rebind it (e.g. v-model) so the prop changes, otherwise the UI stays on the previous array.