Tags

A field that collects a list. Type, press Enter, and the text becomes a tag you can remove.

(Optional)
  • Carrots
  • Onions
  • Tomatoes
Enter or a comma adds one. Backspace on an empty field removes the last.
<TagsInput
  label="Ingredients"
  tagsPosition="outside"
  value={tags}
  onChange={setTags}
  placeholder="Type and press Enter"
/>

Two placements

Not a taste setting: they are two different jobs. Outside is what the design draws: the field stays one line forever and the list grows downward, so a form holding twenty tags does not reflow every time you add one. Inside puts them ahead of the caret, which reads as “these are the value” and is right when there will be three of them, not thirty.

tagsPositionWhereWhy
outsideA row under the fieldFigma 6:4209. The field keeps one line forever.
insideAhead of the caretReads as "these are the value". The field grows a line at a time.

Adding and removing

Enter or a comma commits; commitKeys changes that. Leaving the field commits too, because losing a half-typed tag to a click elsewhere is the most annoying bug this component can have. Backspace on an empty field removes the last tag , the one behaviour everybody tries and most implementations miss, and it is guarded on the field being empty so it never eats a tag while you are still typing one.

Duplicates are dropped silently rather than rejected loudly: you typed something already in the list, the list is already correct.

recipe-form.tsx
const [ingredients, setIngredients] = useState<string[]>([])

<TagsInput
  label="Ingredients"
  value={ingredients}
  onChange={setIngredients}
  validate={(tag) => tag.length <= 24}
  maxTags={12}
/>

Props

PropTypeDefaultNotes
valuestring[]The tags, controlled. `onChange` gives the next array.
defaultValuestring[][]The tags, uncontrolled.
tagsPosition'outside' | 'inside''outside'Where the tags sit.
inputValuestringThe text in the field, if you want to drive it.
onInputValueChange(value: string) => voidFires as that text changes.
commitKeysstring[]['Enter', ',']Keys that turn the text into a tag.
validate(tag, tags) => booleanReturn false and the text stays in the field.
maxTagsnumberStops accepting past this many.
removeLabel(tag: string) => string"Remove {tag}"Accessible name for each remove button.

Shared

The same contract as every other field, with one difference: value is a string[] rather than a string, because the value of this field is a list.

PropTypeDefaultNotes
valuestringControlled value.
defaultValuestringInitial value when uncontrolled.
onChange(value: string) => voidReceives the value, never the event.
labelReactNodeRendered as a real <label> wired by htmlFor.
hintReactNodeHelper text under the field.
errorReactNodeError text. Implies invalid unless invalid says otherwise.
invalidbooleanForces the error styling on or off.
errorKeystring | numberChange it to replay the error animation.
disabledbooleanfalseGreys the field out.
readOnlybooleanfalseReads as filled, stays focusable.
size'sm' | 'md' | 'lg''md'Size scale, where the component defines one.
classNamesRecord<Part, string>Per-part class names.