Tags Input
A list of values the user invents, entered as removable chips — the one Choice control with no options to choose from.
Usage
import {
TagsInput,
TagsInputContext,
TagsInputControl,
TagsInputInput,
TagsInputItem,
TagsInputItemDeleteTrigger,
TagsInputItemInput,
TagsInputItemPreview,
TagsInputItemText,
} from "@kanzo-tech/ui";This is the one control in the Choice family with no option collection. There is nothing to
choose from: the user types a value, presses Enter, and that keystroke creates it. The value is
a string[] the caller had no list for, and every other control on
Controls starts from a list.
That is also the whole reason the anatomy looks unusual. There are no items to map over, so you
iterate the value with the TagsInputContext render prop — each entry gets a preview (its
text plus a delete trigger) and a hidden input of its own, so editing a tag in place is the same
markup as showing it.
<TagsInput defaultValue={["design", "systems"]}>
<TagsInputControl>
<TagsInputContext>
{(api) =>
api.value.map((value, index) => (
<TagsInputItem index={index} key={index} value={value}>
<TagsInputItemPreview>
<TagsInputItemText>{value}</TagsInputItemText>
<TagsInputItemDeleteTrigger />
</TagsInputItemPreview>
<TagsInputItemInput />
</TagsInputItem>
))
}
</TagsInputContext>
<TagsInputInput placeholder="Add tag…" />
</TagsInputControl>
</TagsInput>Reach for it only when the values do not exist beforehand. A tags input cannot tell a typo
from a new value; if the values are known,
Combobox multiple is the right control, and
Controls is where that comparison lives.
Anatomy
TagsInput
├── TagsInputLabel
├── TagsInputControl
│ ├── TagsInputContext renders one item per value
│ │ └── TagsInputItem
│ │ ├── TagsInputItemPreview
│ │ │ ├── TagsInputItemText
│ │ │ └── TagsInputItemDeleteTrigger
│ │ └── TagsInputItemInput
│ ├── TagsInputInput
│ └── TagsInputClearTrigger
└── TagsInputHiddenInputWith field
Inside a Field it behaves like any other control — the label, description and error wiring come
for free. Add TagsInputHiddenInput (and a name on the root) when the values have to submit
with a plain form; it is not rendered for you.
Whatever the poster types becomes a tag. Enter to add.
Keyboard
| Key | Does |
|---|---|
| Enter | Turns the typed text into a tag, or starts editing the highlighted tag when one is highlighted. |
| , | The same as Enter — the delimiter, which is a comma unless you change it. |
| Backspace | With the caret at the start, highlights the last tag; pressing it again deletes that tag and highlights the one before. |
| ← | With the caret at the start, highlights the last tag, then walks backwards through them. |
| → | Walks forwards through the tags, and hands the caret back to the input past the last one. |
| Delete | Deletes the highlighted tag and returns to the input. |
| ↓ / Esc | Drops the highlight and puts the caret back in the input. |
Editing a tag in place needs TagsInputItemInput rendered, and commits on Enter or
cancels on Esc; an emptied tag is deleted rather than saved blank.
API Reference
Props pass straight through to Ark. TagsInput extends TagsInput.Root (value /
defaultValue, onValueChange, max, allowOverflow, editable, disabled, invalid,
readOnly, name, form), so control it with value + onValueChange or leave it
uncontrolled with defaultValue. TagsInputControl adds a size variant (sm · md · lg);
the item text, delete trigger and hidden input each come styled and wired to the machine, but you
place them — including TagsInputHiddenInput, which is not rendered for you. Items are iterated
with the TagsInputContext render prop over api.value.
The controlled root
TagsInputRootProvider takes a machine as value instead of building one, for a surface outside
the compound that has to drive it — a toolbar that clears the tags, a form library that owns the
state. Build the machine with useTagsInput and read it from inside with useTagsInputContext:
const tagsInput = useTagsInput({ defaultValue: ["react"] });
<TagsInputRootProvider value={tagsInput}>
<TagsInputControl>{/* … */}</TagsInputControl>
</TagsInputRootProvider>This is the only component where useX is the machine rather than the context — the hooks
page says why.
TagsInputControlProps is exported, so a wrapper can take the same props without restating them.