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.

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