DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

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.

Playground

Installation

pnpm add @tessinaui/ui

Usage

import { Tokenizer } from "@tessinaui/ui";
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.

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

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
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""ltr"Text direction; also mirrors which arrow enters the chip row
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
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
removeLabelstring"Remove"Accessible name of each remove button; the token is added via aria-describedby
announcementsPartial<TokenizerAnnouncements>—Override any live-region string, for localisation
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""ltr"Text direction

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 · TokenizerProps · TokenizerSkeletonProps · TokenizerSize · TokenizerIntent · TokenizerRounded · TokenizerTone · TokenizerLabelPosition · TokenizerRejection · TokenizerAnnouncements

Accessibility

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 through announcements. 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.

When to use which

Reach for Tokenizer when the values are the user's own words: tags, invitees, keywords, anything you cannot enumerate in advance.

Reach for Combobox with multiple when the values must come from a list you control. Its value can only ever be an item you gave it, which is exactly the guarantee a tokenizer does not make.

They share the chip system — the same scale, the same remove contract — so a row of tokens looks identical either way. What differs is where the values are allowed to come from, and that is not something a prop should switch between.

ToggleGroupToolbar

On this page

PlaygroundInstallationUsageExamplesDefaultSizesIntentsRoundedSeparators and pasteMaximumValidationOn a coloured surfaceDisabledSkeletonRTLAPI ReferenceTokenizerTokenizerSkeletonData attributesExportsAccessibilityWhen to use which