Field.ChipInput
Controlled chips plus input, inside a Field. The field never appends chips. Parent onCommit decides. Adornments stay on Field.Root, outside the chip flow. Chips inside a ChipInput are one field line tall, the same as the input, at every density. chipLayout="row" puts them on a full-width line above the input, inside the ChipInput. Icon and Affix sit on the first line. With no chips it is a one-line field the same height as Field.TextInput. In a row, maxRows counts chip lines only and clips like the inline flow. Label it with Label.Root htmlFor or Root's aria-label.
On this page
<Field.Root aria-label="Tags">
<Field.ChipInput
chips={chips}
inputValue={inputValue}
onInputValueChange={setInputValue}
onCommit={(value) => {
setChips((prev) => (prev.includes(value) ? prev : [...prev, value]));
setInputValue('');
}}
onChipRemove={(index) => setChips((prev) => prev.filter((_, i) => i !== index))}
/>
</Field.Root>
Optional useChipField holds chips and input; spread its getChipInputProps() onto Field.ChipInput.
const field = useChipField();
<Field.Root aria-label="Tags">
<Field.ChipInput {...field.getChipInputProps()} placeholder="Add a tag" />
</Field.Root>
Deferred this slice: built-in clear, paste-split, clickable +N, chip arrow-key roving.
Commit keys
commitKeys={['Enter', ',']}. Unknown keys are dropped.
Rows
When maxRows is set, the flow clamps to that many token-derived rows from the first paint and hides chips until the first layout measure, so SSR does not unfold before +N fold. The visible window is the leading prefix; newest chips fold into +N and unmount from the live flow. A later chip that does not fit at its full width moves into +N. The first chip truncates with an ellipsis when it, +N, and the input cannot share the row; its accessible name stays the full label.
maxRows={1} and {2} under compact and comfortable.
States
Disabled and invalid live on Field.Root.
Icons
Chip remove buttons show the close glyph unless you pass chipRemoveIcon, as on Chip.
Required
required makes a form with no chips invalid, whatever the input holds, since the input is emptied on every commit. The browser checks the chips on a native text input that the ChipInput renders beside its own. It is hidden from sight and from assistive technology, is not a tab stop, and is empty exactly when there are no chips. It has no name, so it sends nothing: a form sends the text in the input under the field's name, as before, and sending the chips is up to you. Inside a Form.Root, a problem shows in the field's ErrorMessage.Root and focus goes to the text input. The text input does not get aria-required, since it is not what must be filled: it is described by the required string in StringsProvider instead. A required on a ChipInput used to need text in the input itself; it now needs at least one chip.
Accessible composition
Todae handles: The input takes id, aria-label, aria-labelledby and aria-describedby from Field.Root, and native disabled and aria-invalid from the Root or a surrounding Fieldset; the chips follow the input's disabled. Each remove button is named after its chip, and focus returns to the input after a remove. A commit key with text in the input calls onCommit with the trimmed text and is kept from the input. When onCommit returns false, Enter and Tab go through, to the form or to the next control, and a character key is still kept from typing. Keys that belong to an IME composition, such as the Enter that ends a Korean word, are left to the input. A held commit key commits once: after the commit, Enter's repeats are kept from the form, and Tab's repeats move focus.
You supply: A name, through a Label whose htmlFor is the Root id or aria-label on the Root. Chip strings in the product's language, through StringsProvider or chipRemoveLabel and overflowLabel.
Watch for: Adding 'Tab' to commitKeys makes Tab commit typed text instead of moving focus.
When to use
Field.ChipInput
When: Controlled chips + one input inside Field.Root. Parent owns commit/append via onCommit, or spreads getChipInputProps() from useChipField to hold that state.
When not: Free-form single string → TextInput. Options from a list → Combobox recipe + OptionList.
Props
| Name | Type | Description |
|---|---|---|
chips Required | string[] | Controlled chip labels. |
inputValue Required | string | Controlled input text. |
onInputValueChange Required | (value: string) => void | Input text changes. |
onCommit Required | (value: string) => boolean | void | Fires on a commit key with a trimmed non-empty value. Does not append chips. Return false when there was nothing to commit: Enter then goes on to the form and Tab moves focus, while a character key such as , is still kept from typing. |
onChipRemove Required | (index: number, value: string) => void | Remove button click or Backspace on an empty input. |
chipRemoveLabel | (value: string) => string | Remove button name. Default is the removeChip string from StringsProvider, Remove {value} in English. |
chipRemoveIcon | React.ReactNode | Chip remove button content, such as an icon from your own set. Without it, the close glyph from GlyphsProvider, else Todae's cross. |
required | boolean | A form with no chips is invalid, whatever the input holds. |
overflowLabel | (n: number) => string | +N name. Default is the moreChips string from StringsProvider, {n} more in English. |
commitKeys | ReadonlyArray<"Enter" | "Tab" | "," | ";" | " "> | Closed allowlist of commit keys. Default: ["Enter"] |
maxRows | number | Caps visual rows. inline: chips plus input. row: chip lines only; the input keeps its own line. Newest chips fold into +N. A chip that does not fit at full width moves into +N. Only a chip wider than the row truncates. |
chipLayout | "inline" | "row" | inline: chips flow beside the input. row: chips sit in a full-width line above the input, inside the ChipInput; Icon and Affix sit on the first line; no chips renders a one-line field the height of TextInput. Default: "inline" |
removeOnBackspace | boolean | Backspace on an empty input removes the last chip. Default: true |
placeholder | string | Native placeholder. |