Chip Input
Multi-value text field that wraps typed entries into removable, keyboard-navigable chips.
Overview
ChipInput combines a text field with a focusable, removable list of chips — type a value and press Enter or the separator character (, by default) to turn it into a chip. Arrow keys move focus between chips, and Backspace/Delete removes a focused (or selected) chip. Each chip renders as a Chip internally, so chipVariant accepts the same color/variant class names Chip documents.
import { ChipInput } from '@chassis-ui/react'
export const Example = () => {
return (
<ChipInput
aria-label="Skills"
defaultValue={['React', 'TypeScript']}
placeholder="Add skill…"
chipVariant="primary smooth"
/>
)
}As a form field
Pass label/help to wrap the input in a .form-field with an associated FormLabel and FormHelp. Pass name to have ChipInput create a hidden <input type="hidden"> per chip — all sharing that name, so a native form submission collects them together (FormData.getAll('skills')).
With required, a chip input with no chips holds its form back from submitting, and its text input takes focus. Text typed there and not added as a chip is not a value. The field then shows as invalid, with its invalidFeedback, until it has a value or the form is reset; set invalid yourself, to false too, to decide how it looks instead.
import { ChipInput } from '@chassis-ui/react'
export const Example = () => {
return (
<ChipInput
label="Skills"
help="Press Enter or , to add a skill."
defaultValue={['React', 'CSS']}
name="skills"
placeholder="Add skill…"
/>
)
}Chip styles
Pass chipVariant with any combination of chassis-css chip modifier classes (e.g. "primary smooth", "danger") to style every chip this input creates.
import { ChipInput } from '@chassis-ui/react'
export const Example = () => {
return (
<div className="vstack gap-md">
<ChipInput
aria-label="Status"
chipVariant="primary"
defaultValue={['Approved', 'Verified']}
placeholder="Add status…"
/>
<ChipInput
aria-label="Issue labels"
chipVariant="danger smooth"
defaultValue={['Bug', 'Critical']}
placeholder="Add label…"
/>
</div>
)
}Controlled values
Pass value/onChange to control the set of chip values yourself. Omit them (use defaultValue instead) to let ChipInput manage its own state.
import { useState } from 'react'
import { ChipInput } from '@chassis-ui/react'
export const Example = () => {
const [values, setValues] = useState<string[]>(['React'])
return (
<div className="vstack gap-sm">
<ChipInput aria-label="Skills" onChange={setValues} placeholder="Add skill…" value={values} />
<div className="form-help">Current values: {values.join(', ') || '(none)'}</div>
</div>
)
}Disabled
Add disabled to make ChipInput non-interactive — existing chips stay visible but can't be
focused, removed, or added to.
import { ChipInput } from '@chassis-ui/react'
export const Example = () => {
return <ChipInput aria-label="Skills" defaultValue={['React', 'CSS']} disabled />
}Keyboard interaction
- Input focused — Enter or the separator character creates a chip from the current value. Backspace/Delete on an empty input, or ← with the cursor at the start, selects and focuses the last chip. Escape clears the input and blurs.
- Chip focused — ←/→ move between chips. Shift+←/→ extends the selection. Cmd/Ctrl+A selects all chips. Backspace/Delete removes the selected chip(s). Escape clears selection.
Accessibility
ChipInput needs an accessible name for the chip group as a whole — pass aria-label,
aria-labelledby, or label (see As a form field). Without one, only the
text input itself falls back to a generic label, leaving the chip list unidentified to screen
reader users.
Scope
Built on React Aria's useTagGroup/useTag for the chip collection (arrow-key navigation, multi-select, removal) plus useTextField for entry — there's no single dedicated "chip input" hook, so the two are composed together with hand-written logic for separator/paste-splitting, matching the plan's own framing of this as the most bespoke of the newer form widgets.
A few Mail.app-style affordances from chassis-css's vanilla chip-input.js aren't wired up here: shift+click range selection and cmd/ctrl+click toggle selection with the mouse, and automatically moving focus back to the input when → or End is pressed on the last chip. Keyboard chip-to-chip navigation, selection, and removal all work; only these secondary mouse/edge-of-list conveniences are out of scope for this pass.
API
ChipInput
| Prop | Type | Default |
|---|---|---|
allowDuplicates? | boolean | false |
Allow the same value to be added more than once. Defaults to false. | ||
aria-label? | string | — |
An accessible label for the chip group, used when there's no visible <label>. | ||
aria-labelledby? | string | — |
Identifies a visible <label> element for the chip group. | ||
chipVariant? | string | default |
Space-separated chassis-css chip modifier classes (e.g. "primary smooth") applied to every
chip. Defaults to "default". | ||
className? | string | — |
| A string of all className you want applied to the base component. | ||
defaultValue? | string[] | — |
| The initial set of chip values (uncontrolled). | ||
disabled? | boolean | — |
| Prevents new chips from being added and makes existing chips non-interactive. | ||
help? | ReactNode | — |
| A description for the field, rendered below the chips. | ||
id? | string | — |
id forwarded to the text input — useful for pairing with a <label for>. | ||
invalid? | boolean | — |
Set component validation state to invalid. Left unset, the component shows as invalid by itself
from a submit it held back (an empty required one, say) until it no longer would, or its
form is reset. Set it, to false too, and it alone decides. | ||
invalidFeedback? | ReactNode | — |
An error message for the field, rendered below the chips when invalid is set, and after a
submit the field held back, until it no longer would. | ||
label? | ReactNode | — |
The field's caption, rendered as a FormLabel associated with the text input. | ||
maxChips? | number | — |
| Maximum number of chips allowed. Omit for no limit. | ||
name? | string | — |
name of an auto-created hidden input per chip, kept in sync with the values, for native
form submission. Omit to skip creating them. | ||
onChange? | ((values: string[]) => void) | — |
| Callback fired whenever a chip is added or removed. | ||
placeholder? | string | — |
| Placeholder shown in the input when empty. | ||
required? | boolean | — |
| Toggle the required state for the component. A required chip input with no chips holds its form back from submitting, and its text input takes focus: text typed there and not added as a chip is not a value. | ||
separator? | string | null | , |
Character that creates a new chip when typed, or pasted text is split on. Set to null to
disable. Defaults to ,. | ||
size? | "sm" | "lg" | — |
| Size the component sm or lg. | ||
valid? | boolean | — |
| Set component validation state to valid. | ||
validFeedback? | ReactNode | — |
A success message for the field, rendered below the chips when valid is set. | ||
value? | string[] | — |
| The set of chip values (controlled). | ||