Skip to main content
Tags input renders tags inside a control, followed by a text input. Tags are added when text is typed and the Enter or Comma key is pressed. DOM focus remains on the input element throughout the interaction. Features
  • Press Enter or Comma to add tags
  • Clear all tags with a clear trigger
  • Add tags by pasting
  • Delete tags with Backspace
  • Edit tags in place after creation
  • Limit the number of tags
  • Navigate tags with the keyboard
  • Custom validation to accept or reject tags

Installation

npm install @zag-js/tags-input @zag-js/react

Usage

import { useMachine, normalizeProps } from "@zag-js/react"
import * as tagsInput from "@zag-js/tags-input"
import { useId } from "react"

export function TagsInput() {
  const service = useMachine(tagsInput.machine, {
    id: useId(),
    defaultValue: ["React", "TypeScript"],
  })
  const api = tagsInput.connect(service, normalizeProps)

  return (
    <div {...api.getRootProps()}>
      <label {...api.getLabelProps()}>Technologies</label>
      <div {...api.getControlProps()}>
        {api.value.map((value, index) => (
          <div key={`${value}-${index}`} {...api.getItemProps({ index, value })}>
            <div {...api.getItemPreviewProps({ index, value })}>
              <span {...api.getItemTextProps({ index, value })}>{value}</span>
              <button {...api.getItemDeleteTriggerProps({ index, value })}>
                ✕
              </button>
            </div>
            <input {...api.getItemInputProps({ index, value })} />
          </div>
        ))}
        <input {...api.getInputProps()} placeholder="Add tag…" />
        <button {...api.getClearTriggerProps()}>Clear all</button>
      </div>
      <input {...api.getHiddenInputProps()} />
    </div>
  )
}
When the input is empty or the caret is at the start, tags can be selected with the ArrowLeft/ArrowRight keys. When a tag has visual focus:
  • Press Enter or double-click to edit. Press Enter again to commit.
  • Press Delete or Backspace to delete the tag.

Controlled tags

Use value and onValueChange to manage tags externally.
const [value, setValue] = useState(["React"])

const service = useMachine(tagsInput.machine, {
  id: useId(),
  value,
  onValueChange(details) {
    setValue(details.value)
  },
})

Limiting the number of tags

Set max to limit how many tags can be added.
const service = useMachine(tagsInput.machine, {
  id: useId(),
  max: 10,
})
To let the count exceed the limit (and handle it yourself), add allowOverflow: true.
const service = useMachine(tagsInput.machine, {
  id: useId(),
  max: 5,
  allowOverflow: true,
})

Custom validation

Provide a validate function to accept or reject a tag before it is added.
const service = useMachine(tagsInput.machine, {
  id: useId(),
  validate(details) {
    // Only allow lowercase alphabetic tags
    return /^[a-z]+$/.test(details.inputValue)
  },
})

Allow duplicates

By default, duplicate tags are prevented. Set allowDuplicates: true for use cases like sentence builders.
const service = useMachine(tagsInput.machine, {
  id: useId(),
  allowDuplicates: true,
})

Blur behavior

Configure what happens when the input loses focus.
const service = useMachine(tagsInput.machine, {
  id: useId(),
  blurBehavior: "add", // "add" | "clear"
})

Paste behavior

Set addOnPaste to create tags from pasted text. Tags are split by the delimiter value.
const service = useMachine(tagsInput.machine, {
  id: useId(),
  addOnPaste: true,
  delimiter: ",",
})

Disable tag editing

Prevent double-click or Enter from entering tag edit mode.
const service = useMachine(tagsInput.machine, {
  id: useId(),
  editable: false,
})

Usage within forms

Set name and render the hidden input for form submission support.
const service = useMachine(tagsInput.machine, {
  id: useId(),
  name: "tags",
})

// In JSX
<input {...api.getHiddenInputProps()} />

API reference

defaultValue
string[]
The initial tag values (uncontrolled).
value
string[]
The controlled tag values. Use with onValueChange.
inputValue
string
The controlled value of the text input. Use with onInputValueChange.
max
number
The maximum number of tags allowed. Defaults to Infinity.
allowOverflow
boolean
Whether to allow adding tags past the max limit.
allowDuplicates
boolean
Whether to allow duplicate tag values. Defaults to false.
editable
boolean
Whether tags can be edited after creation. Defaults to true.
validate
(details: { inputValue: string, value: string[] }) => boolean
A function to validate a tag before it is added. Return false to reject.
blurBehavior
"add" | "clear"
The action to take on the input value when the component loses focus.
addOnPaste
boolean
Whether to add tags from pasted text.
delimiter
string
The delimiter used to split pasted values into individual tags. Defaults to ",".
disabled
boolean
Whether the tags input is disabled.
readOnly
boolean
Whether the tags input is read-only.
invalid
boolean
Whether the tags input is in an invalid state.
name
string
The name attribute for form submission via hidden input.
onValueChange
(details: { value: string[] }) => void
Callback fired when the tag list changes.
onInputValueChange
(details: { inputValue: string }) => void
Callback fired when the text input value changes.
onHighlightChange
(details: { highlightedValue: string | null }) => void
Callback fired when a tag receives visual focus via keyboard navigation.
onValueInvalid
(details: { reason: string }) => void
Callback fired when a tag is rejected by validate or when the max is reached.

Styling

Each part has a data-part attribute.

Focused state

[data-part="root"][data-focus] { /* root when input is focused */ }
[data-part="label"][data-focus] { /* label when input is focused */ }
[data-part="input"]:focus { /* the text input itself */ }

Invalid state

[data-part="root"][data-invalid] { /* invalid root */ }
[data-part="label"][data-invalid] { /* invalid label */ }
[data-part="input"][data-invalid] { /* invalid input */ }

Disabled state

[data-part="root"][data-disabled] { /* disabled root */ }
[data-part="label"][data-disabled] { /* disabled label */ }
[data-part="input"][data-disabled] { /* disabled input */ }
[data-part="control"][data-disabled] { /* disabled control */ }
[data-part="item-preview"][data-disabled] { /* disabled tag */ }

Highlighted (visually focused) tag

[data-part="item-preview"][data-highlighted] { /* keyboard-focused tag */ }

Read-only state

[data-part="root"][data-readonly] { /* read-only root */ }
[data-part="control"][data-readonly] { /* read-only control */ }
[data-part="input"][data-readonly] { /* read-only input */ }
[data-part="label"][data-readonly] { /* read-only label */ }

Keyboard interactions

KeyDescription
EnterAdds the current input value as a tag
,Adds the current input value as a tag (default delimiter)
ArrowLeftWhen input is empty, moves visual focus to the previous tag
ArrowRightMoves visual focus to the next tag
BackspaceDeletes the last tag when the input is empty; deletes the focused tag
DeleteDeletes the visually focused tag
Enter (on focused tag)Enters edit mode for the focused tag
EscapeExits tag edit mode without committing changes

Build docs developers (and LLMs) love