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.
Features
- 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.
Installation
To use the tags input machine in your project, run the following command in your command line:
Anatomy
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.
Usage
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 theuseMachine
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 🔥
Navigating and Editing tags
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 pressEnter
to commit the changes. - Pressing
Delete
orBackspace
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
andvalue
attribute to a hidden input so the tags can be accessed in theFormData
.
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, }), )
Listening for events
During the lifetime of the tags input interaction, here's a list of events we emit:
onValueChange
— invoked when the tag value changes.onHighlightChange
— invoked when a tag has visual focus.onValueInvalid
— invoked when the max tag count is reached or thevalidate
function returnsfalse
.
const [state, send] = useMachine( tagsInput.machine({ onValueChange(details) { // details => { value: string[] } console.log("tags changed to:", details.value) }, onHighlightChange(details) { // details => { value: string } console.log("highlighted tag:", details.value) }, onValueInvalid(details) { console.log("Invalid!", details.reason) }, }), )
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="item-preview"][data-disabled] { /* styles for disabled tag */ }
Highlighted state
When a tag is highlighted via the keyboard navigation or pointer hover, a
data-highlighted
attribute is set on the tag.
[data-part="item-preview"][data-highlighted] { /* 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 */ }
Methods and Properties
Machine Context
The tags input machine exposes the following context properties:
ids
Partial<{ root: string; input: string; clearBtn: string; label: string; control: string; item(opts: ItemProps): string; }>
The ids of the elements in the tags input. Useful for composition.translations
IntlTranslations
Specifies the localized strings that identifies the accessibility elements and their statesmaxLength
number
The max length of the input.delimiter
string
The character that serves has: - event key to trigger the addition of a new tag - character used to split tags when pasting into the inputautoFocus
boolean
Whether the input should be auto-focuseddisabled
boolean
Whether the tags input should be disabledreadOnly
boolean
Whether the tags input should be read-onlyinvalid
boolean
Whether the tags input is invalidallowEditTag
boolean
Whether a tag can be edited after creation. If `true` and focus is on a tag, pressing `Enter`or double clicking will edit the tag.inputValue
string
The tag input's valuevalue
string[]
The tag valuesonValueChange
(details: ValueChangeDetails) => void
Callback fired when the tag values is updatedonHighlightChange
(details: HighlightChangeDetails) => void
Callback fired when a tag is highlighted by pointer or keyboard navigationonValueInvalid
(details: ValidityChangeDetails) => void
Callback fired when the max tag count is reached or the `validateTag` function returns `false`validate
(details: ValidateArgs) => boolean
Returns a boolean that determines whether a tag can be added. Useful for preventing duplicates or invalid tag values.blurBehavior
"clear" | "add"
The behavior of the tags input when the input is blurred - `"add"`: add the input value as a new tag - `"none"`: do nothing - `"clear"`: clear the input valueaddOnPaste
boolean
Whether to add a tag when you paste values into the tag inputmax
number
The max number of tagsallowOverflow
boolean
Whether to allow tags to exceed max. In this case, we'll attach `data-invalid` to the rootname
string
The name attribute for the input. Useful for form submissionsform
string
The associate form of the underlying input element.dir
"ltr" | "rtl"
The document's text/writing direction.id
string
The unique identifier of the machine.getRootNode
() => ShadowRoot | Node | Document
A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.onPointerDownOutside
(event: PointerDownOutsideEvent) => void
Function called when the pointer is pressed down outside the componentonFocusOutside
(event: FocusOutsideEvent) => void
Function called when the focus is moved outside the componentonInteractOutside
(event: InteractOutsideEvent) => void
Function called when an interaction happens outside the component
Machine API
The tags input api
exposes the following methods:
isEmpty
boolean
Whether the tags are emptyinputValue
string
The value of the tags entry input.value
string[]
The value of the tags as an array of strings.valueAsString
string
The value of the tags as a string.count
number
The number of the tags.isAtMax
boolean
Whether the tags have reached the max limit.setValue
(value: string[]) => void
Function to set the value of the tags.clearValue
(id?: string) => void
Function to clear the value of the tags.addValue
(value: string) => void
Function to add a tag to the tags.setValueAtIndex
(index: number, value: string) => void
Function to set the value of a tag at the given index.setInputValue
(value: string) => void
Function to set the value of the tags entry input.clearInputValue
() => void
Function to clear the value of the tags entry input.focus
() => void
Function to focus the tags entry input.getItemState
(props: ItemProps) => ItemState
Returns the state of a tag
Edit this page on GitHub