Chip Input
Free-token entry — Enter or comma commits, delimited pastes split into chips, and per-chip validation keeps invalid entries visible instead of dropping them.
- Status
- Since
0.4.0- Accessibility pattern
- labelled token input
Last updated
Enter or comma commits; Backspace in the empty input removes the last chip.
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Chip Input before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify chip-inputInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/chip-inputRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
The same command installs the registry items it composes: @vegastack/input-group, @vegastack/tag-group, @vegastack/use-announcer.
Usage
import { ChipInput } from "@/components/ui/chip-input";
<ChipInput aria-label="Tags" value={tags} onValueChange={setTags} />;ChipInput fills the gap neither existing component can:
TagGroup is a display list with no input, and
Combobox's value model is selection-from-items —
it cannot commit an arbitrary token. The field chrome is
InputGroup — border, focus border, invalid
hairline, disabled wash, all of it imported — and the inner control is
InputGroupInput, which is Input flattened into the
box. The only thing this component adds to that chrome is a field that wraps:
it grows with its chips instead of holding one 32px line. The chips are real
Tags, 24px remove targets included.
Scope
| Behaviour | Where it lives |
|---|---|
| Suggestions / autocomplete | A Combobox composition — deliberately not built in |
| Persisting the list | Host, via onValueChange |
| Chip colour semantics | Not here — Tag hue is decorative; invalid is the only state |
+N overflow collapse | TagGroup — display-list semantics, wrong for an editable field |
Examples
Per-chip validation
Entries are added and marked, not silently dropped: a pasted list of twenty
addresses with two typos shows twenty chips with two flagged
(data-invalid, destructive border) — and the field stays invalid until they
are fixed or removed.
Invalid entries stay visible and flagged — paste a list and fix the typos instead of losing them.
Inside a Field
Inside a Field the inner input is labelled by FieldLabel — a click on
the label focuses it — described by the rendered FieldDescription and FieldError, and marked
invalid by the Field or by an invalid chip. name submits every chip with a form, one value per
chip.
Search matches any of these words.
Maximum entries
max caps the number of chips. An entry past the cap is not added, stays in the input, and the
reason is announced and shown under the field until the next keystroke — "Up to 3 entries" by
default, or your own copy from maxLabel. A skipped duplicate and an invalid entry show the same
way.
Up to 3 colours.
States
Empty, filled and disabled; the invalid field is the per-chip validation example above. Click or Tab into a field to see the focus state: the box's border darkens subtly and nothing fills; an invalid field keeps its destructive border while focused.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
allowDuplicates | boolean | false | Allow the same chip twice. When false, a duplicate entry is rejected and
announced. |
aria-label | string | — | Accessible name for the inner input. Inside a Field the FieldLabel names it; standalone,
the control must never be unnamed. |
className | string | — | Extra classes for the root column (data-slot="chip-input-root") that holds the box and
its refusal line — where layout classes (flex-1, col-span-*, a min width) belong. |
defaultValue | string[] | [] | Initial chips for uncontrolled use. |
disabled | boolean | false | Disables the field and every chip's remove button. |
inputRef | React.Ref<HTMLInputElement> | — | Ref forwarded to the inner <input>. |
max | number | — | The most chips the field accepts. At the cap, further entries are not added and the reason is announced. |
maxLabel | ((max: number) => string) | (max) => `Up to ${max} entries` | The copy announced when an entry is refused at max. |
name | string | — | Submits every chip with a surrounding <form> under this name — one value per chip, the way
a multi-select posts. |
normalize | ((raw: string) => string) | (raw) => raw.trim() | Normalise a raw entry before it is committed (and before duplicate checking). The default trims whitespace. |
onValueChange | ((value: string[]) => void) | — | Fired with the next chip list on every add or remove. |
placeholder | string | — | Placeholder for the inner input while the field has room. |
ref | React.Ref<HTMLDivElement> | — | Ref forwarded to the field box <div> (data-slot="chip-input"), which carries the
data-invalid and data-disabled state. |
splitOn | RegExp | /[,\n]/ | Pattern the paste handler splits on, in addition to the Enter/comma commit keys. |
validate | ((chip: string) => boolean) | — | Per-chip validity. Invalid entries are still **added**, marked with
data-invalid on their chip, described as invalid for assistive tech, and
flip the whole field invalid until fixed or removed —
a pasted list keeps every entry visible instead of silently dropping the
malformed ones. |
value | string[] | — | Controlled chip list. Pair with onValueChange; omit for uncontrolled use. |
Data attributes and CSS variables on ChipInput
| Attribute | Values |
|---|---|
data-disabled | "" |
data-field-group | "" |
data-invalid | "" |
data-slot | "chip-input" | "chip-input-notice" | "chip-input-root" |
Accessibility
- The inner input must carry an accessible name (
aria-label) — it is the control assistive tech interacts with. - Accepted, invalid, duplicate, and removed outcomes are announced through a polite live region; invalid chips also carry a text description, never colour alone. Refusals and invalid entries also show as one line under the field (hidden from assistive tech, which already hears the announcement), cleared by the next keystroke.
- Every chip's remove button is named (
Remove …) with a 24px hit area (fromTag). - The field is a field group: while the input inside holds focus its border
darkens subtly (
border-ring/50, easing in over 150ms) and no background changes — text entry's exception to FOC-14 (MK 2026-09-27). An invalid field keeps its destructive border in every state; focus never replaces it. This matches the Combobox chips field.
| Key | Action |
|---|---|
| Enter / , | Commit the draft as a chip. |
| Backspace (empty input) | Remove the last chip. |
| Tab | Move between the input and chip remove buttons. |
| Contract | States tested |
|---|---|
| Behaviour | empty, chips, invalid-chip, duplicate-rejected, disabled |
| Accessibility | keyboard, labeled, status-announcement, semantic-html |
| Visual | default, focus, invalid, disabled, dark |