Token Definitions
Token definitions describe the structured keys your search input recognizes. Each definition configures a key name, display label, available options, and validation rules.
Basic Definition
Section titled “Basic Definition”const tokens = [ { key: 'status', label: 'Status', options: [ { value: 'active', label: 'Active' }, { value: 'inactive', label: 'Inactive' }, ], },]| Property | Type | Description |
|---|---|---|
key | string | Internal key name (e.g. 'status') |
label | string? | Display label in the suggestion dropdown. Defaults to key. |
options | TokenOption[] | (query, signal) => ... | Static or async options |
icon | ReactNode? | Icon shown in the key suggestion dropdown |
exclusive | boolean? | Only one instance of this key allowed |
strict | boolean? | Value must match a provided option |
pattern | RegExp? | Value must match this regular expression |
negatable | boolean? | Enable not: prefix for negation |
renderDropdown | (props) => ReactNode | Custom dropdown renderer |
focusOnOpen | boolean? | Move focus into a custom dropdown when it opens. Defaults to true when renderDropdown is set. |
Exclusive Tokens
Section titled “Exclusive Tokens”Set exclusive: true to allow only one instance of a key in the query. If a user tries to add a second status: token, it won’t appear in the suggestion dropdown:
{ key: 'status', exclusive: true, options: [...] }Strict Validation
Section titled “Strict Validation”Set strict: true to require values match one of the provided options. Invalid values will not be highlighted as tokens:
{ key: 'priority', strict: true, options: [...] }Pattern Validation
Section titled “Pattern Validation”Set pattern to a regular expression to constrain values without locking them to a fixed option list — the middle ground between strict and fully open. Values that don’t match are not highlighted as tokens. This is ideal for free-form-but-structured values like dates:
{ key: 'created', label: 'Created', pattern: /^\d{4}-\d{2}-\d{2}$/, // ISO date, e.g. created:2026-07-15}pattern is independent of strict: strict requires the value to be in the option list, pattern requires it to match the regex, and neither leaves the token fully open.
The pattern is tested against the value exactly as typed, so case sensitivity is governed by the regex itself (add the i flag if needed) — unlike strict matching, which is always case-insensitive. As with strict validation, the token currently being edited is exempt so it isn’t stripped mid-typing.
Async Options
Section titled “Async Options”Options can be fetched dynamically. The function receives the current query text and an AbortSignal for cancellation:
{ key: 'customer', label: 'Customer', options: async (query, signal) => { const res = await fetch(`/api/customers?q=${query}`, { signal }) return res.json() },}Value Resolution
Section titled “Value Resolution”When text containing async token labels is pasted or loaded (e.g. from a shared URL), the component automatically calls the options function to resolve display labels back to their technical values. During resolution, the submit button receives aria-busy and disabled attributes — the input remains fully interactive.
Resolution uses exact label matching: only options where option.label exactly matches the pasted text (case-insensitive) are resolved. Free-text values that don’t match any option are left as-is.
Resolved values include an id field in the parsed segment, while free-text values do not — use this to distinguish between the two in your onSearch handler:
onSearch={(segments) => { for (const seg of segments) { if (seg.type === 'token' && seg.key === 'customer') { if (seg.id) { // Resolved from option — seg.value is the technical value } else { // Free-text — seg.value is what the user typed } } }}}Negatable Tokens
Section titled “Negatable Tokens”Enable negation with negatable: true. A “Not” option appears in the value dropdown, allowing users to negate values (e.g. status:not:active):
{ key: 'status', negatable: true, options: [...] }Customize the negation label for i18n via the <DropdownNotOption> slot:
<TokenizedSearch tokens={tokens} onSearch={handleSearch}> <TokenizedSearch.Dropdown> <TokenizedSearch.DropdownNotOption>nicht</TokenizedSearch.DropdownNotOption> </TokenizedSearch.Dropdown></TokenizedSearch>Custom Dropdowns
Section titled “Custom Dropdowns”Replace the default dropdown for a token with a fully custom renderer:
{ key: 'date', renderDropdown: ({ value, onChange, close }) => ( <DatePicker value={value} onChange={(v) => onChange(v, true)} // true = close dropdown /> ),}The renderDropdown callback receives:
| Prop | Type | Description |
|---|---|---|
value | string | Current token value |
siblings | TokenSegment[] | The other tokens in the query (all except the one being edited) |
onChange | (value, closeDropdown?) => void | Update the value |
close | () => void | Close the dropdown |
Focus and embedded widgets
Section titled “Focus and embedded widgets”Custom dropdowns often embed focusable widgets — a date picker, a combobox, buttons. Two things make this “just work”:
focusOnOpen(defaulttrue). When the dropdown opens, focus moves into it. This is required for widgets built on pointer-press libraries (e.g. react-aria): in WebKit/Safari, pressing an element while the search editor’scontenteditablestill holds focus starts the editor’s text-selection drag, which captures the pointer and cancels the press — so the widget’s click never fires. Focusing the dropdown on open takes focus off the editor and avoids this. SetfocusOnOpen: falseonly for dropdowns driven purely by continued typing in the editor.- The popover stays open while you interact with it. Dismissal is driven by
a pointer press outside the widget (plus keyboard tab-out), not by focus
loss. So you do not need to guard controls with
onMouseDown={(e) => e.preventDefault()}to stop the popover closing — and you shouldn’t, because preventing the mousedown default breaks press detection for react-aria-based widgets in WebKit.
{ key: 'created', // focusOnOpen defaults to true — the calendar receives focus on open, so // day clicks register even in Safari, and the popover stays open. renderDropdown: ({ value, onChange }) => ( <DateCalendar value={parseDate(value)} onChange={(date) => onChange(formatDate(date), true)} /> ),}Option Shape
Section titled “Option Shape”Each option in the options array has:
| Property | Type | Description |
|---|---|---|
value | string | Internal value (emitted in segments) |
label | string? | Display text. Defaults to value. |
id | string? | Optional identifier passed through to parsed segments |