Skip to main content
View Zag.js on Github
Join the Discord server

Tags Input

Tag inputs render tags inside an input, followed by an actual text input. By default, tags are added when text is typed in the input field and the Enter or Comma key is pressed. Throughout the interaction, DOM focus remains on the input element.



  • Typing in the input and pressing enter will add new items.
  • Clear button to reset all tags values.
  • Add tags by pasting into the input.
  • Delete tags on backspace.
  • Edit tags after creation.
  • Limit the number of tags.
  • Navigate tags with keyboard.
  • Custom validation to accept/reject tags.


To use the tags input machine in your project, run the following command in your command line:

npm install @zag-js/tags-input @zag-js/react # or yarn add @zag-js/tags-input @zag-js/react


To set up the tags input correctly, you'll need to understand its anatomy and how we name its parts.

Each part includes a data-part attribute to help identify them in the DOM.

On a high level, the tags input consists of:

  • Root: The root container for the tags input.
  • Label: The accessible label for the tags input textbox.
  • Control: The container for the tags and the input's textbox.
  • Tag: The element that represents a tag.
  • Input: The textbox for adding new tags.
  • Hidden Input: The hidden input that holds the value of the tags input. Used within forms.
  • Clear Button: The button that clears all tags.

Each tag consists of:

  • Tag Input: The input element that is used to edit a tag.
  • Tag Delete Button: The button to delete a tag.


First, import the tags input package into your project

import * as tagsInput from "@zag-js/tags-input"

The tags input package exports two key functions:

  • machine — The state machine logic for the tags input widget.
  • connect — The function that translates the machine's state to JSX attributes and event handlers.

You'll also need to provide a unique id to the useMachine hook. This is used to ensure that every part has a unique identifier.

Next, import the required hooks and functions for your framework and use the tags input machine in your project 🔥

import * as tagsInput from "@zag-js/tags-input" import { useMachine, normalizeProps } from "@zag-js/react" export function TagsInput() { const [state, send] = useMachine( tagsInput.machine({ id: "1", value: ["React", "Vue"], }), ) const api = tagsInput.connect(state, send, normalizeProps) return ( <div {...api.rootProps}> {, index) => ( <span key={index}> <div {...api.getTagProps({ index, value })}> <span>{value} </span> <button {...api.getTagDeleteTriggerProps({ index, value })}> &#x2715; </button> </div> <input {...api.getTagInputProps({ index, value })} /> </span> ))} <input placeholder="Add tag..." {...api.inputProps} /> </div> ) }

When the input has an empty value or the caret is at the start position, the tags can be selected by using the arrow left and arrow right keys. When "visual" focus in on any tag:

  • Pressing Enter or double clicking on the tag will put the it in edit mode, allowing the user change its value and press Enter to commit the changes.
  • Pressing Delete or Backspace will delete the tag that has "visual" focus.

Setting the initial tags

To set the initial tag values, pass the value property in the machine's context.

const [state, send] = useMachine( tagsInput.machine({ value: ["React", "Redux", "TypeScript"], }), )

Removing all tags

The tags input will remove all tags when the clear button is clicked. To remove all tags, use the provided clearButtonProps function from the api.

//... <div {...api.controlProps}> <input {...api.inputProps} /> <button {...api.clearButtonProps} /> </div> //...

To programmatically remove all tags, use the api.clearAll() method that's available in the connect.

Usage within forms

The tags input works when placed within a form and the form is submitted. We achieve this by:

  • ensuring we emit the input event as the value changes.
  • adding a name and value attribute to a hidden input so the tags can be accessed in the FormData.

To get this feature working you need to pass a name option to the context and render the hiddenInput element.

const [state, send] = useMachine( tagsInput.machine({ name: "tags", value: ["React", "Redux", "TypeScript"], }), )

Limiting the number of tags

To limit the number of tags within the component, you can set the max property to the limit you want. The default value is Infinity.

When the tag reaches the limit, new tags cannot be added except the allowOverflow option is passed to the context.

const [state, send] = useMachine( tagsInput.machine({ max: 10, allowOverflow: true, }), )

Validating Tags

Before a tag is added, the machine provides a validate function you can use to determine whether to accept or reject a tag.

A common use-case for validating tags is preventing duplicates or validating the data type.

const [state, send] = useMachine( tagsInput.machine({ validate(details) { return !details.values.includes(details.inputValue) }, }), )

Blur behavior

When the tags input is blurred, you can configure the action the machine should take by passing the blurBehavior option to the context.

  • "add" — Adds the tag to the list of tags.
  • "clear" — Clears the tags input value.
const [state, send] = useMachine( tagsInput.machine({ blurBehavior: "add", }), )

Paste behavior

To add a tag when a arbritary value is pasted in the input element, pass the addOnPaste option.

When a value is pasted, the machine will:

  • check if the value is a valid tag based on the validate option
  • split the value by the delimiter option passed
const [state, send] = useMachine( tagsInput.machine({ addOnPaste: true, }), )

Disable tag editing

by default the tags can be edited by double clicking on the tag or focusing on them and pressing Enter. To disable this behavior, pass the allowEditTag: false

const [state, send] = useMachine( tagsInput.machine({ allowEditTag: false, }), )


During the lifetime of the tags input interaction, here's a list of events we emit:

  • onChange — invoked when the tag value changes.
  • onHighlight — invoked when a tag has visual focus.
  • onInvalid — invoked when the max tag count is reached or the validate function returns false.
const [state, send] = useMachine( tagsInput.machine({ onChange(tags) { console.log("tags changed to:", tags) }, onHighlight(tag) { console.log("highlighted tag:", tag) }, onInvalid(err) { console.log("Invalid!", err) }, }), )

Methods and Properties

The tag input's api exposes the following methods:

  • value — The value of the added tags.
  • count — The total number of the added tags.
  • valueAsString — The value of the added tags represented as a string.
  • inputValue — The value of the tags input field.
  • setValue() — Used to set the value of the accordion that will be expanded.
  • addValue() — Function used to add a new tag value.
  • deleteValue() — Function used to remove a specific tag value.
  • clearAll() — Function used to clear all tag values.
const api = connect(state, send) api.value // => ["tag-1", "tag-2"] api.setValue(["item-2", "item-3"]) // => ["item-2", "item-3"] api.valueAsString // => "['item-2', 'item-3']" api.addValue("item-4") // => ["item-2", "item-3", "item-4"] api.deleteValue("item-2") // => ["item-3", "item-4"] api.clearAll() // => []

Styling guide

Earlier, we mentioned that each accordion part has a data-part attribute added to them to select and style them in the DOM.

Focused state

The combobox input is focused when the user clicks on the input element. In this focused state, the root, label, input.

[data-part="root"][data-focus] { /* styles for root focus state */ } [data-part="label"][data-focus] { /* styles for label focus state */ } [data-part="input"]:focus { /* styles for input focus state */ }

Invalid state

When the tags input is invalid by setting the invalid: true in the machine's context, the data-invalid attribute is set on the root, input, control, and label.

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

Disabled state

When the tags input is disabled by setting the disabled: true in the machine's context, the data-disabled attribute is set on the root, input, control and label.

[data-part="root"][data-disabled] { /* styles for disabled state for root */ } [data-part="label"][data-disabled] { /* styles for disabled state for label */ } [data-part="input"][data-disabled] { /* styles for disabled state for input */ } [data-part="control"][data-disabled] { /* styles for disabled state for control */ }

When a tag is disabled, the data-disabled attribute is set on the tag.

[data-part="tag"][data-disabled] { /* styles for disabled tag */ }

Selected state

When a tag is selected via the keyboard navigation or pointer hover, a data-selected attribute is set on the tag.

[data-part="tag"][data-selected] { /* styles for visual focus */ }

Readonly state

When the tags input is in its readonly state, the data-readonly attribute is set on the root, label, input and control.

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

Edit this page on GitHub

On this page