Tag Input

PreviousNext

A multi-value input — type a tag and press Enter, or pick one from the suggestions, and it becomes a chip.

Installation

pnpm dlx shadcn@latest add https://matos-ui.com/r/tag-input.json

Usage

import { TagInput } from '@/components/matos-ui/tag-input'
const [tags, setTags] = useState<string[]>([])

<TagInput value={tags} onValueChange={setTags} />

Examples

With suggestions and a cap

<TagInput
  label="Stack"
  suggestions={['React', 'Vue', 'Svelte', 'Solid']}
  max={5}
  value={tags}
  onValueChange={setTags}
/>

Free entry with validation

<TagInput
  validate={(tag) => tag.length <= 20 || 'Under 20 characters.'}
  placeholder="Type, press Enter, paste a,b,c"
/>

In a form

<form action={submit}>
  <TagInput name="labels" defaultValue={['bug']} />
  <button type="submit">Save</button>
</form>

Each tag posts as its own hidden <input name="labels">.

How it's built

  • Base UI does the hard part. @base-ui/react/combobox with multiple carries the ARIA (role="combobox", the chip group, the listbox) and the keyboard: typeahead, arrows through the list, Backspace on an empty input removes the last chip, / move between chips, Esc closes.
  • Free entry sits on top. Enter (or Tab) with text that isn't a suggestion adds it as a tag — unless an item is highlighted, in which case Base UI's selection wins. delimiter splits typed or pasted text (a, b, c → three tags). creatable={false} turns it back into a chip-style multi-select.
  • Chips are surface, not fill. A soft --muted chip, a pressable remove button. They add on spring.fast from scale: 0.8 and reflow their neighbours with layout; removing one shrinks it out under AnimatePresence mode="popLayout".
  • The popup rides the ladder. surfaceClasses(useSurface() + 2) — the dropdown convention — with the same data-open / data-closed transition and useExitAnimation guard the Select uses. It only renders when there are suggestions or something to create.
  • Rejections shake. A tag that fails validate (or max, or a duplicate) isn't added; the field does one attentionShake and shows the message.
  • prefers-reduced-motion drops the chip scale and the shake, keeps the fades.

Reference

TagInput Props

PropTypeDefaultDescription
valuestring[]Controlled tags.
defaultValuestring[][]Initial tags when uncontrolled.
onValueChange(tags: string[]) => voidFires with the full tag list.
suggestionsArray<string | { value, label }>Autocomplete options. Omit for pure free entry.
creatablebooleantrueAllow tags that aren't suggestions.
maxnumberCap the number of tags.
allowDuplicatesbooleanfalse
delimiterRegExp/[,\n\t]/Split typed / pasted text into several tags.
validate(tag: string) => boolean | stringReject a tag; a string is the shown message.
namestringOne hidden input per tag, for form posting.
label / description / errorReactNodeField composition.
disabledbooleanfalse
size"sm" | "md" | "lg""md"