Kanzo UI
Forms

Tags Input

A list of values the user invents, entered as removable chips — the one Choice control with no options to choose from.

Ark UI
night-work
bring-rope

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
└── TagsInputHiddenInput

With 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.

escort

Whatever the poster types becomes a tag. Enter to add.

A string list in state

value and onValueChange bind it to a string[], and max stops the machine accepting at a limit. That composition is the whole of a "keywords field", and there is no TagsInput that takes values and draws the items for you: a component that flattens this compound into an array is the compound with fewer capabilities — no className on an item, no custom preview, no asChild — so it is written out where it is used, as below.

salvage
coastal
2 of 4

Keyboard

KeyDoes
EnterTurns 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.
BackspaceWith 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.
DeleteDeletes the highlighted tag and returns to the input.
↓ / EscDrops 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.

On this page