Skip to content

TagInput

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/tag-input.json
See it liveTyped, capped, and in three sizes.→

The difference from Combobox multiple is where the values come from. A combobox chooses from a collection that exists; this makes values that do not exist until someone types them — labels, keywords, recipients. If the set is known in advance, reach for the combobox: it can filter, and this cannot.

<TagInput
value={tags}
onValueChange={setTags}
aria-label={t('tags')}
removeLabel={(tag) => t('remove', { tag })}
/>

Blur commits what was typed. A word left in the box when the reader presses Save is a word they believe they entered, and losing it silently is the single commonest complaint about fields of this shape. Enter commits too, but nobody reliably presses it before reaching for the mouse.

Backspace on an empty box removes the last tag. A list built by typing should be unbuildable by typing; without this the only way back is the mouse. While there is text in the box, Backspace belongs to the text.

Enter is only intercepted when there is something to commit. Otherwise it belongs to the form around the field — swallowing it unconditionally breaks submitting by keyboard, which is how a form is finished without a mouse.

It does not validate. What makes a tag acceptable — its length, its charset, whether it must already exist — is your rule, and a component that guessed would be wrong in both directions. Refuse a value by not putting it in value; the field is controlled, so nothing appears that you did not accept.

Duplicates are refused and whitespace is trimmed, because a tag list is a set and " docs " is the same tag as "docs". Enter on a blank box does nothing rather than adding a tag nobody can see.

removeLabel is yours, and required — the same rule as Chip, for the same reason: a primitive with a string of its own ships in English to every reader who does not read English.

Props

Read from the types in ui/tag-input.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.

PropTypeDefault
sizesm | md | lg'md'
valuerequiredstring[]The tags. Controlled: the product owns the list.
onValueChangerequired(value: string[]) => voidCalled with the next list.
placeholderstringPlaceholder for the text box, shown when there is room for it.
aria-labelstringNames the field. Required - a box of chips with no label names nothing.
aria-labelledbystringOr points at a label element, when there is a visible one.
removeLabelrequired(tag: string) => stringHow a tag's remove button is named, for a screen reader. Takes the tag, so the product can say "Remove documentation" in its own language.
maxnumberCap on how many tags may be added.
disabledbooleanDims the field and stops it taking input or removing tags.
childrenReactNodeDrawn after the input, inside the box - a counter, a hint.
classNamestring
idstringPut on the text box, so a Field label can point at it.

Accessibility

The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 15 rules applied and passed, and none failed. The accessibility report has every item.

History

Each release that changed ui/tag-input.tsx - the file a product copies - newest first.

  1. v0.41.0
    • docs read every props table from the component's types ee3b159
  2. v0.36.0
    • fix set typed text at 16px under a finger 967db1d
  3. v0.20.0
    • feat five components for the harder half of a form cfb317a