Accessible, unstyled TagsInput with typed tags, delimiters, paste splitting, and form association.
|
|
| Package |
@vizejs/ui/tags-input |
| Maturity |
stable |
| Own the source |
vize lib pull tags-input |
| Requires |
context, controllable-state, id |
| Aliases |
tags input, chips input, token field, tag editor |
| Covers |
Reka UI TagsInput, Ark UI Tags Input, Mantine TagsInput |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { TagsInput, TagsInputRoot, TagsInputInput, TagsInputItem, TagsInputItemDelete, TagsInputItemText } from "@vizejs/ui/tags-input";
Or copy the source into your project with vize lib pull tags-input (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Controlled tags input bound with v-model, labelled by visible text, with removable tags. -->
<script setup lang="ts">
import { ref, useId } from "vue";
import {
TagsInputInput,
TagsInputItem,
TagsInputItemDelete,
TagsInputItemText,
TagsInputRoot,
} from "@vizejs/ui/tags-input";
const topics = ref<readonly string[]>(["vue", "vite"]);
const labelId = useId();
</script>
<template>
<div>
<span :id="labelId">Topics</span>
<TagsInputRoot v-model="topics" name="topics" :max="5" :aria-labelledby="labelId">
<template #default="{ tags }">
<TagsInputItem v-for="(tag, index) in tags" :key="tag" :value="tag" :index>
<TagsInputItemText />
<TagsInputItemDelete>×</TagsInputItemDelete>
</TagsInputItem>
<TagsInputInput placeholder="Add a topic" />
</template>
</TagsInputRoot>
<output>{{ topics.join(", ") }}</output>
</div>
</template>
API
Source: src/families/form/tags-input/tags-input-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Id of the native text input, so a <label for> can name the field. null and undefined select a deterministic fallback. |
modelValue |
readonly T[] |
undefined |
Controlled tags. undefined selects uncontrolled behavior. |
defaultValue |
readonly T[] |
[] |
Initial uncontrolled tags and the value restored by reset and native form reset. |
parseTag |
(text: string) => T | null |
undefined |
Convert trimmed input text into a tag, or return null to reject it with reason "parse". Omitting it keeps the text as-is, which is only sound for string tags; non-string tag types must provide a parser. |
tagText |
(tag: T) => string |
String(tag) |
Display and form-submission text for one tag. |
by |
TagsInputBy<T> |
Object.is |
Duplicate-detection equality: an object property name or a comparator. |
validate |
TagsInputValidator<T> |
undefined |
Custom validation. Return false or a message string to reject a candidate. |
delimiters |
readonly string[] |
[","] |
Strings that commit the current text. Enter always commits. |
addOnPaste |
boolean |
true |
Split pasted text on delimiters, line breaks, and tabs into several tags. |
addOnBlur |
boolean |
false |
Commit pending text when the input loses focus. |
allowDuplicates |
boolean |
false |
Accept tags equal to an existing tag under by. |
max |
number |
undefined |
Maximum number of tags. undefined means unlimited. |
editable |
boolean |
false |
Allow Enter, F2, or double-click on a focused tag to edit it inline. |
disabled |
boolean |
false |
Disable input, tag focus, removal, and form submission. |
readonly |
boolean |
false |
Keep tags focusable and submitted while preventing every change. |
required |
boolean |
false |
Require at least one tag for native constraint validation. |
name |
string |
undefined |
Native form field name. Each tag submits one entry under this name. |
form |
string |
undefined |
Id of the form owning the hidden submission inputs. |
dir |
TagsInputDirection |
"ltr" |
Reading direction used by ArrowLeft and ArrowRight between tags. |
ariaLabel |
string |
undefined |
Accessible name for the text input when no label supplies one. |
ariaLabelledby |
string |
undefined |
Space-separated ids that label the text input. |
ariaDescribedby |
string |
undefined |
Space-separated ids that describe the text input. |
ariaErrormessage |
string |
undefined |
Id of the validation error message used while invalid. |
ariaInvalid |
TagsInputAriaInvalid |
false |
Invalid state announced on the text input. |
Events
| Event |
Payload |
Description |
update:modelValue |
[value: readonly T[]] |
Fired when the tag list requests a new controlled value. |
add |
[tag: T, index: number, source: TagsInputAddSource] |
Fired after a tag is accepted, with its index and input source. |
remove |
[tag: T, index: number, source: TagsInputRemoveSource] |
Fired after a tag is removed, with its former index and input source. |
edit |
[tag: T, previous: T, index: number] |
Fired after an inline edit replaces a tag. |
invalid |
[event: TagsInputInvalidEvent<T>] |
Fired when a candidate tag is rejected by parsing, duplicates, max, or validation. |
Slots
| Slot |
Slot props |
Description |
default |
TagsInputSlotState<T> |
TagsInputItem children and the TagsInputInput. Receives the current tags and state. |
Exposed
| Member |
Type |
Description |
add |
— |
|
addTag |
— |
|
clear |
— |
|
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
focus |
— |
|
id |
— |
|
inputValue |
— |
|
remove |
— |
|
reset |
— |
|
setInputValue |
— |
|
state |
— |
|
tags |
— |
|
Source: src/families/form/tags-input/tags-input-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Id of the native text input, so a <label for> can name the field. null and undefined select a deterministic fallback. |
modelValue |
readonly T[] |
undefined |
Controlled tags. undefined selects uncontrolled behavior. |
defaultValue |
readonly T[] |
[] |
Initial uncontrolled tags and the value restored by reset and native form reset. |
parseTag |
(text: string) => T | null |
undefined |
Convert trimmed input text into a tag, or return null to reject it with reason "parse". Omitting it keeps the text as-is, which is only sound for string tags; non-string tag types must provide a parser. |
tagText |
(tag: T) => string |
String(tag) |
Display and form-submission text for one tag. |
by |
TagsInputBy<T> |
Object.is |
Duplicate-detection equality: an object property name or a comparator. |
validate |
TagsInputValidator<T> |
undefined |
Custom validation. Return false or a message string to reject a candidate. |
delimiters |
readonly string[] |
[","] |
Strings that commit the current text. Enter always commits. |
addOnPaste |
boolean |
true |
Split pasted text on delimiters, line breaks, and tabs into several tags. |
addOnBlur |
boolean |
false |
Commit pending text when the input loses focus. |
allowDuplicates |
boolean |
false |
Accept tags equal to an existing tag under by. |
max |
number |
undefined |
Maximum number of tags. undefined means unlimited. |
editable |
boolean |
false |
Allow Enter, F2, or double-click on a focused tag to edit it inline. |
disabled |
boolean |
false |
Disable input, tag focus, removal, and form submission. |
readonly |
boolean |
false |
Keep tags focusable and submitted while preventing every change. |
required |
boolean |
false |
Require at least one tag for native constraint validation. |
name |
string |
undefined |
Native form field name. Each tag submits one entry under this name. |
form |
string |
undefined |
Id of the form owning the hidden submission inputs. |
dir |
TagsInputDirection |
"ltr" |
Reading direction used by ArrowLeft and ArrowRight between tags. |
ariaLabel |
string |
undefined |
Accessible name for the text input when no label supplies one. |
ariaLabelledby |
string |
undefined |
Space-separated ids that label the text input. |
ariaDescribedby |
string |
undefined |
Space-separated ids that describe the text input. |
ariaErrormessage |
string |
undefined |
Id of the validation error message used while invalid. |
ariaInvalid |
TagsInputAriaInvalid |
false |
Invalid state announced on the text input. |
Events
| Event |
Payload |
Description |
update:modelValue |
[value: readonly T[]] |
Fired when the tag list requests a new controlled value. |
add |
[tag: T, index: number, source: TagsInputAddSource] |
Fired after a tag is accepted, with its index and input source. |
remove |
[tag: T, index: number, source: TagsInputRemoveSource] |
Fired after a tag is removed, with its former index and input source. |
edit |
[tag: T, previous: T, index: number] |
Fired after an inline edit replaces a tag. |
invalid |
[event: TagsInputInvalidEvent<T>] |
Fired when a candidate tag is rejected by parsing, duplicates, max, or validation. |
Slots
| Slot |
Slot props |
Description |
default |
TagsInputSlotState<T> |
TagsInputItem children and the TagsInputInput. Receives the current tags and state. |
Exposed
| Member |
Type |
Description |
element |
HTMLDivElement | null |
Rendered root element. |
id |
string |
Id used by the text input. |
tags |
readonly T[] |
Current tags. |
inputValue |
string |
Current uncommitted input text. |
state |
TagsInputState |
Stable root state token. |
add |
(text: string) => boolean |
Parse, validate, and append one tag from text. Reports whether it was added. |
addTag |
(tag: T) => boolean |
Validate and append one already-parsed tag. Reports whether it was added. |
remove |
(index: number) => boolean |
Remove the tag at index. Reports whether it was removed. |
clear |
() => boolean |
Remove every tag. Reports whether the value changed. |
setInputValue |
(text: string) => void |
Replace the uncommitted input text. |
focus |
(options?: FocusOptions) => void |
Move DOM focus to the text input. |
reset |
() => boolean |
Restore defaultValue and clear the input text. Reports whether tags changed. |
Source: src/families/form/tags-input/tags-input-input.vue
Props
| Prop |
Type |
Default |
Description |
placeholder |
string |
undefined |
Native placeholder shown while the input is empty. |
autocomplete |
string |
"off" |
Native autocomplete hint. |
Exposed
| Member |
Type |
Description |
element |
HTMLInputElement | null |
Rendered native text input. |
focus |
(options?: FocusOptions) => void |
Move DOM focus to the input. |
Source: src/families/form/tags-input/tags-input-item.vue
Props
| Prop |
Type |
Default |
Description |
value (required) |
T |
required |
Tag value rendered by this item, normally one entry of the root slot tags. |
index (required) |
number |
required |
Zero-based position of value in the root tag list. |
disabled |
boolean |
false |
Keep this tag visible but unfocusable, uneditable, and unremovable. |
editLabel |
string |
"Edit tag" |
Accessible name prefix of the inline edit input. |
Slots
| Slot |
Slot props |
Description |
default |
TagsInputItemSlotState<T> |
Tag content, normally TagsInputItemText and TagsInputItemDelete. Hidden while editing. |
Exposed
| Member |
Type |
Description |
element |
HTMLSpanElement | null |
Rendered tag element. |
focus |
(options?: FocusOptions) => void |
Move DOM focus to this tag. |
remove |
() => boolean |
Remove this tag. Reports whether it was removed. |
edit |
() => boolean |
Enter inline editing when the root is editable. Reports whether editing started. |
Source: src/families/form/tags-input/tags-input-item-delete.vue
Props
| Prop |
Type |
Default |
Description |
label |
string |
"Remove" |
Accessible name prefix followed by the tag text, e.g. "Remove vue". |
ariaLabel |
string |
undefined |
Full accessible name override. |
Slots
| Slot |
Slot props |
Description |
default |
TagsInputItemSlotState<unknown> |
Button content such as an icon. Receives the owning item state. |
TagsInputItemText
Source: src/families/form/tags-input/tags-input-item-text.vue
Slots
| Slot |
Slot props |
Description |
default |
TagsInputItemSlotState<unknown> |
Custom tag text. Receives the owning item state; defaults to the resolved tag text. |
Behavior
Contract
TagsInput is a headless compound field that turns typed or pasted text into a
typed tag list. tags-input-root.vue (generic="T") owns the value, parsing,
validation, duplicate detection, the max limit, native form association, and
form reset. tags-input-item.vue renders one focusable tag, handles roving
arrow-key focus, deletion, and inline editing. tags-input-item-text.vue
renders the resolved tag text, tags-input-item-delete.vue renders a
non-tabbable remove button, and tags-input-input.vue renders the native text
input that commits candidates.
The text input is the single Tab stop. Tags are tabindex="-1" elements with
role="group", aria-roledescription="tag", and an accessible name equal to
the tag text; group is used because each tag contains its own remove button,
which a button or option role would not permit. Styling is consumer-owned
through parts, slots, CSS, and data attributes.
Without parseTag, tags are the trimmed typed strings; non-string tag types
must provide parseTag (and usually tagText, which also produces each
submitted form value).
Public Surface
| Surface |
Contract |
TagsInputRoot props |
id, modelValue, defaultValue, parseTag, tagText, by, validate, delimiters, addOnPaste, addOnBlur, allowDuplicates, max, editable, disabled, readonly, required, name, form, dir, aria props |
TagsInputRoot emits |
update:modelValue(tags), add(tag, index, source), remove(tag, index, source), edit(tag, previous, index), invalid(event) |
TagsInputRoot slots |
default(state) with tags, inputValue, count, full, disabled, readonly, invalid, state |
TagsInputRoot expose |
element, id, tags, inputValue, state, add, addTag, remove, clear, setInputValue, focus, reset |
TagsInputItem props / slots |
value, index, disabled, editLabel; default(state) |
TagsInputItem expose |
element, focus, remove, edit |
TagsInputItemText |
default(state) slot; falls back to the resolved tag text |
TagsInputItemDelete |
label (prefix, default "Remove"), ariaLabel override; default(state) slot |
TagsInputInput |
placeholder, autocomplete; expose element, focus |
| Parts |
root, item, item-text, item-delete, item-edit, input |
| Data attributes |
root data-vize-ui="tags-input", data-state, data-count, data-full, data-invalid, data-disabled, data-readonly; item data-state, data-index, data-active, data-disabled |
Normative Behavior
| # |
State |
Input |
Outcome |
Proving test |
| T1 |
any |
render |
tags, ids (<id>-tag-<index>), aria wiring, remove names, and data attributes are published |
renders tags, input semantics, ids, and data attributes |
| T2 |
input has text |
Enter |
trimmed text is parsed and appended; default prevented; input clears; add(..., "enter") |
Enter and delimiter keys commit trimmed text while empty Enter passes through |
| T3 |
input empty |
Enter |
not prevented, so native implicit form submission still works |
Enter and delimiter keys commit trimmed text while empty Enter passes through |
| T4 |
any |
delimiter key |
default prevented and the text commits with source "delimiter" |
Enter and delimiter keys commit trimmed text while empty Enter passes through |
| T5 |
any |
input event containing delimiters |
complete segments commit; the trailing fragment stays in the input |
typed delimiters commit complete segments and keep the trailing fragment |
| T6 |
addOnPaste |
paste with delimiters, line breaks, or tabs |
default prevented; every segment commits with source "paste"; undelimited paste stays native |
pasting delimited or multi-line text splits it into tags |
| T7 |
candidate rejected |
commit |
invalid emits parse, duplicate, max, or invalid (+ validator message); rejected text stays editable |
duplicates, max, parse failures, and validators emit invalid events |
| T8 |
by / allowDuplicates |
commit equal tag |
a property name or comparator defines equality; allowDuplicates accepts equals |
allowDuplicates and by keys control duplicate detection |
| T9 |
caret at start, tags |
Backspace / ArrowLeft in input |
focus moves to the last tag (first Backspace focuses, the next one deletes) |
keyboard matrix moves focus between tags and removes with Backspace and Delete |
| T10 |
text before caret |
Backspace / ArrowLeft in input |
native caret editing; focus stays in the input |
Backspace and ArrowLeft keep caret editing when text precedes the caret |
| T11 |
tag focused |
ArrowLeft / ArrowRight / Home / End |
previous / next tag (no wrap), next past the last tag and End focus the input, Home the first tag |
keyboard matrix moves focus between tags and removes with Backspace and Delete |
| T12 |
tag focused |
Backspace / Delete |
removes the tag; Backspace focuses the previous tag, Delete the tag now in the slot, else the input |
keyboard matrix moves focus between tags and removes with Backspace and Delete |
| T13 |
dir="rtl" |
arrows |
horizontal arrows are mirrored |
rtl direction mirrors horizontal arrows |
| T14 |
any |
remove button click |
removes with source "delete-button"; focus stays on a tag or returns to the input |
the delete button removes its tag and keeps focus in the field |
| T15 |
editable |
Enter / F2 / double-click on tag |
inline edit input opens focused; Enter commits (edit), invalid keeps editing, Escape cancels, blur commits |
editable tags edit inline with Enter, F2, and double-click |
| T16 |
disabled / readonly |
any change |
disabled removes tags from focus and submission; readonly keeps them focusable and submitted; nothing changes |
disabled and readonly fields block every change |
| T17 |
name in a form |
submit / reset |
one hidden entry per tag (tagText); required blocks empty submission; form reset restores defaultValue |
submits one form entry per tag, validates required, and restores on form reset |
| T18 |
controlled |
commit |
emits the requested list and waits for the parent |
controlled tags wait for the parent to accept updates |
| T19 |
addOnBlur / composition |
blur / IME Enter or delimiter |
blur commits only with addOnBlur; keys during composition never commit |
addOnBlur commits on blur and IME composition never commits |
| T20 |
any |
pointerdown on empty root space |
default prevented and focus moves to the input |
pointerdown on empty root space focuses the input |
| T21 |
any |
exposed API |
add, addTag, remove, clear, setInputValue, focus, reset, item focus/edit share one state |
exposed root and item methods share state |
| T22 |
no provider |
mount part alone |
throws VIZE_UI_CONTEXT_MISSING |
items and parts require a matching TagsInput provider |
| T23 |
SSR |
render twice / hydrate |
byte-identical markup; hydration keeps nodes and ids with no diagnostics |
renders byte-identical TagsInput markup across isolated SSR requests, hydrates tags, ids, and hidden inputs without replacing SSR nodes |