DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
CheckboxRadioSwitchTextareaNumberFieldOTP InputSliderCircularSliderRatingSelectPickerComboboxSearchSearchSuggestTokenizerSegmentedControl
AI agentsContributing
  1. Components
  2. Tokenizer

Tokenizer

A free-text multi-value input — type, press Enter, and the text becomes a removable chip. Not constrained to a list; Combobox is the control for that.

$ pnpm add @tessinaui/ui

When to use

  • Values in the user's own words: tags, invitees, keywords, anything you cannot enumerate in advance.
  • A list the user builds by typing. Enter or a separator turns the text into a removable token, and pasting a comma- or line-separated list commits every part at once.
  • Light rules on what gets in. max, duplicate refusal and validate turn a bad entry away and say why, while the text stays in the input to fix.
  • Lists that are edited often. The chip row is one Tab stop, and Backspace removes the last token, so correcting a list never means hunting for a small target.

When not to use

  • When values must come from a list you control. Use Combobox with multiple: its value can only be an item you gave it, which a tokenizer does not guarantee. They share the chip system, so the two look identical; what differs is where values may come from, and a prop should not switch that.
  • For a single value or a sentence. Use Textarea for longer text; a token is a short value, not a paragraph.
  • For a few fixed options. Use Checkbox or ToggleGroup when the choices are known and few.
  • For tokens that carry a status. intent describes the field, never a token. A status belongs on a Chip or a Badge.
  • For values edited in place. A token is added or removed, not edited inline.

Installation

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import { Tokenizer } from "@tessinaui/ui";
example.tsx
const [tags, setTags] = useState<string[]>([]);

<Tokenizer label="Tags" value={tags} onChange={setTags} placeholder="Add a tag…" />

Examples

Default

Sizes

The chips read one step down the chip scale, so a token never crowds the field it sits in.

Intents

intent is the state of the field — its border, its supporting text, and aria-invalid on the input. It never tints an individual token: a token is a value, not a status.

Rounded

One prop cascades to the field, its chips and their remove buttons, so rounded="none" leaves no curved corner anywhere.

Separators and paste

Typing a separator commits the part before it and keeps the rest in the input. Pasting text that contains separators commits every part as one batch — one state update and one announcement, not one per token.

Pasting into the middle of an in-progress token appends rather than splices; splicing would need caret bookkeeping for a case nobody does.

Maximum

Past the cap, commits are refused and announced. The input stays editable, so the text you typed is still there to fix.

Validation

validate runs after the empty, max and duplicate checks. Return false for the default message, or a string to supply your own — it is announced verbatim. Inside a pasted batch it sees the tokens committed earlier in that same batch.

Error message

errorMessage replaces supportingText while present, implies intent="error", joins the input's aria-describedby and is announced politely. Use it for problems with the whole field, such as a required list left empty; validate refuses a single token.

On a coloured surface

Inside a <Surface> the field inherits tone="on-color" and passes it to every chip. An explicit tone always wins.

Disabled

Skeleton

Wrap the real component when the tokens already exist — the placeholder is then its exact box by construction. Reach for TokenizerSkeleton only when there is nothing to wrap yet.

RTL

Pass dir="rtl" to mirror the field and the arrow key that enters the chip row. It also switches the built-in strings — each remove button's name and every announcement — to Arabic, with the right plural form for each count. Leave dir unset and the tokenizer inherits the page's direction but keeps its English strings; for another language, pass every strings entry.

API Reference

Tokenizer

Extends the HTML attributes of the wrapping <div>; className styles that wrapper and fieldClassName styles the bordered box inside it.

PropTypeDefaultDescription
valuestring[]—The tokens, controlled. The in-progress text is never part of it
defaultValuestring[][]Starting tokens when uncontrolled
onChange(value: string[]) => void—Called with the whole next list after every commit and removal, never with a rejected candidate
size"xs" | "sm" | "md" | "lg" | "xl""md"Density; the chips read one step down
intent"none" | "error" | "warning" | "success" | "info""none"Semantic state of the field, never of a token
rounded"none" | "sm" | "md" | "lg" | "full""full"Corner treatment; the chips follow it
variant"outline" | "filled""outline"Shell: the bordered well, or the tinted well with no outline until focus or an intent
tone"default" | "on-color"inherited from <Surface>Colour recipe for a coloured or dark surface
disabledbooleanfalseDims the field once — its chips do not dim again — disables every chip and remove button, and sets aria-disabled on the root
dir"ltr" | "rtl"inheritedText direction; also mirrors which arrow enters the chip row, and "rtl" picks the Arabic strings. Stamped only when set
labelstring—Visible label, associated with the input
labelPosition"outside-top" | "outside-left""outside-top"Where the label sits. There is no inside: the field grows as chips wrap
supportingTextstring—Helper row under the field, wired to the input's aria-describedby
errorMessageReactNode—Error row under the field. Replaces supportingText, implies intent="error", sets aria-invalid and is announced politely
placeholderstring—Placeholder, hidden once there is at least one token
maxnumber—Hard cap on the number of tokens
allowDuplicatesbooleanfalseLet the same string in twice. Comparison is exact and case-sensitive
validate(token: string, tokens: string[]) => boolean | string—Gate a candidate. A returned string refuses and is announced verbatim
separatorsRegExp | null/[,\n]/What splits typed and pasted text. null turns splitting off
removeLabelstringstrings.removeAccessible name of each remove button; the token is added via aria-describedby
stringsPartial<TokenizerStrings>English, or Arabic under dir="rtl"Override any built-in string per entry: remove and the announcement templates ({token}, {n}, {added})
announcementsPartial<TokenizerAnnouncements>—Replace any announcement with a function; wins over strings
requiredboolean—Asterisk on the label, aria-required on the input
idstringgeneratedId for the input; the label's htmlFor follows it
fieldClassNamestring—Class for the bordered field box

TokenizerSkeleton

PropTypeDefaultDescription
size"xs" | "sm" | "md" | "lg" | "xl""md"Same scale as the Tokenizer
rounded"none" | "sm" | "md" | "lg" | "full""full"Same map as the Tokenizer
showLabelbooleanfalseDraw the label row
labelPosition"outside-top" | "outside-left""outside-top"Mirrors the component
showSupportingTextbooleanfalseDraw the supporting-text row
tokensnumber3How many token placeholders to draw
tokenCharsnumber6Length of a real token; the bar is sized from it
dir"ltr" | "rtl"inheritedText direction; "rtl" announces "Loading" in Arabic. Stamped only when set

Data attributes

AttributeOnValue
data-slot="tokenizer"wrapper—
data-slot="tokenizer-field"the bordered box—
data-slot="tokenizer-chips"the chip row—
data-slot="tokenizer-input"the text input—
data-slot="tokenizer-status"the live region—
data-size | data-intent | data-tonewrapperthe resolved value
data-disabledwrapperpresent when disabled

Exports

Tokenizer · TokenizerSkeleton · tokenizerContainerVariants · TOKENIZER_STRINGS · TokenizerStrings · TokenizerProps · TokenizerSkeletonProps · TokenizerSize · TokenizerIntent · TokenizerRounded · TokenizerTone · TokenizerLabelPosition · TokenizerRejection · TokenizerAnnouncements

Accessibility

VoiceOver + Safari: passed 2026-09-14. The input reads "Tags, Press Enter or comma to add, edit text"; each chip's remove button reads "Remove design, button"; Backspace removal announces "research removed. 1 token remaining." NVDA and JAWS are untested.

Tab model. The chip row is one Tab stop, not one per chip — Tab reaches the row, Tab again reaches the input. Without that, a field holding eight tokens would be nine Tab stops before the next control.

Keyboard.

KeyWhereWhat it does
EnterinputCommits the trimmed text. An empty input is a no-op and is not prevented, so Enter still submits an enclosing form
Backspaceinput, emptyRemoves the last token. Focus stays in the input
Escapeinput, with textClears the in-progress text and stops there, so an enclosing dialog does not also close
ArrowLeftinput, caret at startMoves focus to the last chip. ArrowRight in RTL
ArrowLeft | ArrowRightchip rowMove between chips. Home and End jump to the ends
ArrowRightchip row, last itemReturns focus to the input. ArrowLeft in RTL. The row does not wrap
Backspace | Deletea focused chipRemoves that chip and hands focus to the next one, then the previous, then the input

Announcements. A polite live region owned by the field — never by a chip, which would be unmounted by the very removal it needs to announce — reports:

EventAnnouncement
one token added"{token} added. {n} tokens."
several added at once"{N} tokens added. {n} total."
token removed"{token} removed. {n} tokens remaining."
duplicate refused"{token} is already in the list."
validate refused"{token} is not a valid entry.", or your string verbatim
max reached"Limit reached. {max} tokens maximum."

Every string is overridable per entry through strings, or as a function through announcements; under dir="rtl" they default to Arabic. An empty commit and an Escape-clear announce nothing.

Naming. The label names the input. Each remove button is named "Remove" and described by its own token, so it announces as "Remove, design" rather than as an unnamed button. The chip row itself carries no role and no name — the field already has one, and a second would be a second name for one thing.

SearchSuggestSegmentedControl

On this page

When to useWhen not to useInstallationUsageExamplesDefaultSizesIntentsRoundedSeparators and pasteMaximumValidationError messageOn a coloured surfaceDisabledSkeletonRTLAPI ReferenceTokenizerTokenizerSkeletonData attributesExportsAccessibilityKeyboard.