Tags
A field that collects a list. Type, press Enter, and the text becomes a tag you can remove.
<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.
| tagsPosition | Where | Why |
|---|---|---|
outside | A row under the field | Figma 6:4209. The field keeps one line forever. |
inside | Ahead of the caret | Reads 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.
const [ingredients, setIngredients] = useState<string[]>([])
<TagsInput
label="Ingredients"
value={ingredients}
onChange={setIngredients}
validate={(tag) => tag.length <= 24}
maxTags={12}
/>Props
| Prop | Type | Default | Notes |
|---|---|---|---|
value | string[] | — | The tags, controlled. `onChange` gives the next array. |
defaultValue | string[] | [] | The tags, uncontrolled. |
tagsPosition | 'outside' | 'inside' | 'outside' | Where the tags sit. |
inputValue | string | — | The text in the field, if you want to drive it. |
onInputValueChange | (value: string) => void | — | Fires as that text changes. |
commitKeys | string[] | ['Enter', ','] | Keys that turn the text into a tag. |
validate | (tag, tags) => boolean | — | Return false and the text stays in the field. |
maxTags | number | — | Stops 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.
| Prop | Type | Default | Notes |
|---|---|---|---|
value | string | — | Controlled value. |
defaultValue | string | — | Initial value when uncontrolled. |
onChange | (value: string) => void | — | Receives the value, never the event. |
label | ReactNode | — | Rendered as a real <label> wired by htmlFor. |
hint | ReactNode | — | Helper text under the field. |
error | ReactNode | — | Error text. Implies invalid unless invalid says otherwise. |
invalid | boolean | — | Forces the error styling on or off. |
errorKey | string | number | — | Change it to replay the error animation. |
disabled | boolean | false | Greys the field out. |
readOnly | boolean | false | Reads as filled, stays focusable. |
size | 'sm' | 'md' | 'lg' | 'md' | Size scale, where the component defines one. |
classNames | Record<Part, string> | — | Per-part class names. |