A token field allows users to enter text with inline tokens. Use it to build AI prompt fields, tag inputs, structured search fields, mention inputs, and multi-select comboboxes.
Vanilla CSS theme
--tint CSS variable used by the Vanilla CSS examples.Value
TokenField is controlled via a TokenFieldValue value, which represents a sequence of text and token segments. Use the value or defaultValue prop to set the value, and onChange to handle user input.
Value: Hello @username!
import {Token, TokenField} from './TokenField';
import {TokenFieldValue} from 'react-stately/useTokenFieldState';
import {useState} from 'react';
function Example() {
let [value, setValue] = useState(
new TokenFieldValue([
{type: 'text', text: 'Hello '},
{type: 'token', text: '@username'},
{type: 'text', text: '!'}
])
);
return (
<>
<TokenField value={value} onChange={setValue} label="Message">
{segment => <Token>{segment.text}</Token>}
</TokenField>
<p>Value: {value.toString()}</p>
</>
);
}
Tokenization
Extend the TokenFieldValue class to customize how text is converted into tokens. Override the tokenize method to parse user input, and use createFieldValue to return new instances of your subclass when the value changes.
import {Token, TokenField} from './TokenField';
import {TokenizingFieldValue} from './TokenizingFieldValue';
<TokenField
allowsNewlines
defaultValue={TokenizingFieldValue.tokenize(
'This example automatically tokenizes #hashtags and @usernames in the text.',
/(?<=\s|^)[#@]\S+(?=\s)/g
)}
label="Message">
{segment => <Token>{segment.text}</Token>}
</TokenField>
Autocomplete
Combine TokenField with Autocomplete to provide inline completions such as @mentions and slash commands. Use TokenFieldValue.findText to locate the anchor character, and tokenFieldPositionToDOMRange to position a Popover relative to the filter text.
import {Autocomplete} from 'react-aria-components/Autocomplete';
import {Text} from 'react-aria-components/Text';
import {Token, TokenField} from './TokenField';
import {tokenFieldPositionToDOMRange} from 'react-aria/useTokenField';
import {TokenFieldValue} from 'react-aria-components/TokenField';
import {Menu, MenuItem} from './Menu';
import {Popover} from './Popover';
import {useMemo, useRef, useState} from 'react';
type Item = {username: string} | {command: string; description: string};
function Example() {
let inputRef = useRef(null);
let [value, setValue] = useState(
new TokenFieldValue([
{type: 'text', text: 'This example has autocomplete for '},
{type: 'token', text: '@usernames'},
{type: 'text', text: ' and '},
{type: 'token', text: '/commands'}
])
);
let [filterAnchor, filterValue] = useMemo(() => {
let filterAnchor = value.findText(value.caretPosition, TokenFieldValue.Direction.Backward, /(?<=^|\s)[@/]/);
if (filterAnchor != null) {
let filterValue = value.slice(filterAnchor, value.caretPosition).toString();
return [filterAnchor, filterValue];
}
return [null, null];
}, [value]);
let items: Item[] = [];
if (filterValue != null && filterValue.startsWith('/')) {
items = slashCommands.filter(item => item.command.includes(filterValue.slice(1)));
} else if (filterValue != null && filterValue.startsWith('@')) {
items = usernames.filter(item => item.username.includes(filterValue.slice(1)));
}
return (
<Autocomplete>
<TokenField value={value} onChange={setValue} label="Prompt" allowsNewlines inputRef={inputRef}>
{segment => <Token>{segment.text}</Token>}
</TokenField>
<Popover
triggerRef={inputRef}
isOpen={filterAnchor != null && items.length > 0}
isNonModal
hideArrow
placement="bottom start"
trigger="MenuTrigger"
getTargetRect={target => {
return tokenFieldPositionToDOMRange(target, filterAnchor!).getBoundingClientRect();
}}>
<Menu items={items} dependencies={[filterAnchor]}>
{item => (
<MenuItem
id={'username' in item ? item.username : item.command}
onAction={() => {
setValue(value =>
value.replaceRangeWithSegments(
filterAnchor!,
value.caretPosition,
[
{
type: 'token',
text: 'username' in item ? '@' + item.username : item.command
},
{type: 'text', text: ' '}
],
false
)
);
}}>
<Text slot="label">{'username' in item ? item.username : item.command}</Text>
{'description' in item ? <Text slot="description">{item.description}</Text> : null}
</MenuItem>
)}
</Menu>
</Popover>
</Autocomplete>
);
}
Tag field
Use a custom TokenFieldValue to build a tag input. This example converts comma, space, or newline separated text into tokens.
import {Token, TokenField} from './TokenField';
import {TagFieldValue} from './TagFieldValue';
<TokenField
allowsNewlines
defaultValue={
new TagFieldValue([
{type: 'token', text: 'Architecture'},
{type: 'token', text: 'Design'},
{type: 'token', text: 'Development'},
{type: 'token', text: 'Marketing'},
{type: 'token', text: 'Sales'}
])
}
label="Categories">
{segment => <Token>{segment.text}</Token>}
</TokenField>
Search
Token fields can be used to build advanced search UIs with structured query tokens. This example suggests filters based on the current text segment.
import {Autocomplete} from 'react-aria-components/Autocomplete';
import {Collection} from 'react-aria-components/Collection';
import {Token, TokenField} from './TokenField';
import {Header, Menu, MenuItem, MenuSection} from './Menu';
import {Popover} from './Popover';
import {useRef, useState} from 'react';
import {TokenFieldValue} from 'react-stately/useTokenFieldState';
function Example() {
let inputRef = useRef(null);
let [value, setValue] = useState(
new TokenFieldValue([{type: 'token', text: 'From: Alice Smith'}])
);
let last = value.segments[value.caretPosition.index];
let filterText = last?.type === 'text' ? last.text : null;
let suggestions: {name: string; items: string[]}[] = [];
if (filterText != null) {
let users = usernames
.filter(item => item.username.includes(filterText))
.map(u => u.username)
.slice(0, 5);
if (users.length > 0) {
if (
!value.segments.some(
segment => segment.type === 'token' && segment.text.startsWith('From: ')
)
) {
suggestions.push({
name: 'From',
items: users
});
}
suggestions.push({
name: 'To',
items: users
});
}
suggestions.push({
name: 'Subject',
items: [filterText]
});
}
return (
<Autocomplete>
<TokenField role="searchbox" value={value} onChange={setValue} label="Search" inputRef={inputRef}>
{segment => <Token>{segment.text}</Token>}
</TokenField>
<Popover
triggerRef={inputRef}
isOpen={suggestions.length > 0}
isNonModal
hideArrow
placement="bottom start"
style={{width: 'var(--trigger-width)'}}
trigger="MenuTrigger">
<Menu items={suggestions}>
{section => (
<MenuSection>
<Header>{section.name}</Header>
<Collection items={section.items}>
{item => (
<MenuItem
id={section.name + '-' + item}
onAction={() => {
setValue(value =>
value.replaceRangeWithSegments(
{index: value.caretPosition.index, offset: 0},
value.caretPosition,
[{type: 'token', text: section.name + ': ' + item}],
false
)
);
}}>
{item}
</MenuItem>
)}
</Collection>
</MenuSection>
)}
</Menu>
</Popover>
</Autocomplete>
);
}
ComboBox
Use TokenField as the input for a ComboBox to build a multi-select field with tokens. Synchronize the TokenFieldValue with the ComboBox value and inputValue props using a custom segment list class.
import {ComboBox} from 'react-aria-components/ComboBox';
import {Token, TokenField} from './TokenField';
import {ComboBoxItem, ComboBoxListBox} from './ComboBox';
import {FieldButton, Label} from './Form';
import {Popover} from './Popover';
import {ComboBoxTokenFieldValue} from './ComboBoxTokenFieldValue';
import {ChevronDown} from 'lucide-react';
import {useState} from 'react';
function Example() {
let [value, setValue] = useState(new ComboBoxTokenFieldValue([]));
return (
<ComboBox
selectionMode="multiple"
style={{width: 500, maxWidth: '100%'}}
value={value.getSelectedKeys()}
inputValue={value.getInputValue()}
onChange={keys => setValue(value.setSelectedKeys(keys))}>
<Label>Users</Label>
<div className="combobox-field">
<TokenField value={value} onChange={setValue} placeholder="Select users" style={{paddingInlineEnd: 36}}>
{segment => <Token>{segment.text}</Token>}
</TokenField>
<FieldButton>
<ChevronDown />
</FieldButton>
</div>
<Popover hideArrow className="combobox-popover">
<ComboBoxListBox items={usernames}>
{state => <ComboBoxItem id={state.username}>{state.username}</ComboBoxItem>}
</ComboBoxListBox>
</Popover>
</ComboBox>
);
}
API
<TokenField>
<Label />
<TokenInput>{segment => <Token />}</TokenInput>
<Text slot="description" />
</TokenField>
TokenField
| Name | Type | |
|---|---|---|
allowsNewlines | boolean | |
Whether the token field allows newlines. | ||
isReadOnly | boolean | |
Whether the token field is read only. | ||
isDisabled | boolean | |
Whether the token field is disabled. | ||
children | ChildrenOrFunction | |
The children of the component. A function may be provided to alter the children based on component state. | ||
value | TokenFieldValue | |
The current value (controlled). | ||
defaultValue | TokenFieldValue | |
The default value (uncontrolled). | ||
onChange | | |
Handler that is called when the value changes. | ||
Default className: react-aria-TokenField
| Render Prop | CSS Selector |
|---|---|
isDisabled | CSS Selector: [data-disabled]
|
| Whether the token field is disabled. | |
isReadOnly | CSS Selector: [data-readonly]
|
| Whether the token field is read only. | |
TokenInput
| Name | Type | |
|---|---|---|
children | | |
A function that renders a token for each segment in the token field. | ||
isDisabled | boolean | |
Whether the hover events should be disabled. | ||
Default className: react-aria-TokenInput
| Render Prop | CSS Selector |
|---|---|
isHovered | CSS Selector: [data-hovered]
|
| Whether the token input is currently hovered with a mouse. | |
isFocused | CSS Selector: [data-focused]
|
| Whether the token input is focused, either via a mouse or keyboard. | |
isFocusVisible | CSS Selector: [data-focus-visible]
|
| Whether the token input is keyboard focused. | |
isDisabled | CSS Selector: [data-disabled]
|
| Whether the token input is disabled. | |
isReadOnly | CSS Selector: [data-readonly]
|
| Whether the token input is read only. | |
Token
| Name | Type | |
|---|---|---|
children | ChildrenOrFunction | |
The children of the component. A function may be provided to alter the children based on component state. | ||
Default className: react-aria-Token
| Render Prop | CSS Selector |
|---|---|
isSelected | CSS Selector: [data-selected]
|
| Whether the token is selected. | |
isDisabled | CSS Selector: [data-disabled]
|
| Whether the token is disabled. | |
TokenFieldValue
Properties
| Name | Type | |
|---|---|---|
Direction | any | |
segments | readonly TokenFieldSegment | |
The text and token segments in the list. | ||
caretPosition | Position | |
The caret position. | ||
Methods
constructor | ||
| Create a new list with the given segments. | ||
withCaretPosition | ||
| Create a new list with the caret position set to the given position. | ||
replaceRange(
start: Position,
end: Position,
text: string,
coalesce: any
): this | ||
| Replace the text between two positions with new text. | ||
replaceRangeWithSegments(
start: Position,
end: Position,
insert: TokenFieldSegment | ||
| Replace the text between two positions with new segments. | ||
findBoundaryWithSegmenter(
position: Position,
segmenter: Intl.Segmenter,
direction: any
): Position | null | ||
| Find the boundary before or after a position using an Intl.Segmenter. | ||
findLineBoundary | ||
| Find a line boundary before or after a position. | ||
findText(
position: Position,
direction: any,
search: string | RegExp
): Position | null | ||
| Find a string or regular expression match before or after a position. | ||
delete(
position: Position,
segmenter: Intl.Segmenter,
direction: any,
coalesce: any
): this | ||
| Delete text at a position using a segmenter. | ||
deleteLine(
position: Position,
direction: any,
coalesce: any
): this | ||
| Delete text to the next or previous line break. | ||
slice | ||
| Create a new list containing a subset of the segments. | ||
toString | ||
| Convert the list to a string. | ||
undo | ||
| Returns the previous list in the undo history. | ||
redo | ||
| Returns the next list in the redo history. | ||
endCoalescing | ||
| End coalescing undo/redo history. | ||