Field

Shared chrome. Field.Root is the compound scope. The bordered shell is Root. Value lives on Field.TextInput, Field.Textarea, or Field.ChipInput. Adornments are order-only: put Icon / Affix before the input to lead, after to trail. No side prop. Label is a sibling, not a compound part. Every part inside the field (input, Icon, Affix, chips) is one field line tall, and the shell's padding is the inset.

Label

Label is a sibling, not a compound part. It takes the same id you pass to Field.Root. Wrap the pair in one .labelled-field. The gap is field.label.gap. See Label for the API.

const id = useId()

<div className="labelled-field">
	<Label htmlFor={id}>Email</Label>
	<Field.Root id={id}>
		<Field.TextInput value={v} onChange={(event) => setV(event.target.value)} />
	</Field.Root>
</div>

Label works with Icon, Affix, and ChipInput.

$USD

Affix is text, not a control. Decorative Icon is aria-hidden. Interactive Icon keeps its name. Clear is not a built-in prop.

$.00

Messages

Description and ErrorMessage sit under the shell as siblings. They are not Field parts. Each owns margin-block-start. Put aria-describedby on Field.Root. Root copies it onto the input. Do not set it on Field.TextInput.

const describedBy = invalid ? `${hintId} ${errorId}` : hintId

<div className="labelled-field">
	<Label htmlFor={id}>Email</Label>
	<Field.Root id={id} invalid={invalid} aria-describedby={describedBy}>
		<Field.TextInput value={email} onChange={(event) => setEmail(event.target.value)} />
	</Field.Root>
	<Description id={hintId}>Use your work address.</Description>
	{invalid ? <ErrorMessage id={errorId}>{error}</ErrorMessage> : null}
</div>
We only use this for billing receipts.Enter an address that contains an @.

Textarea

Multi-line text. Height is CSS only: rows sets the height and the drag floor, and maxRows sets the cap. autoGrow uses field-sizing: content where the browser has it, and grows with no cap when maxRows is omitted. Browsers without field-sizing stay at rows and scroll inside. Do not set an inline height when autoGrow is on; it overrides the growth. Padding sits on the textarea, inside the shell.

autoGrow with maxRows={6}.

maxRows without rows starts at min(3, maxRows).

Drag-resize with autoGrow off stops at rows and at maxRows.

Compact, comfortable, and spacious.

Dark appearance, for the native scrollbar.

Label, description, error, invalid, and disabled.

A short description of the work.Use at least 8 characters.

Counter

Field.Counter shows a live count for Field.TextInput or Field.Textarea. It takes its own row. Write it after the input so the server HTML includes the count. One counter per Field.Root. The input stays controlled; defaultValue is not read.

Up to 20 characters15 characters left

On the same row, set flex-basis: auto on the Counter and still write it last. On a textarea that inline count sits on the last line. The bare field under each pair is the height check.

Up to 20 characters15 characters left
Up to 80 characters

A narrow field. An inline count wider than the row can wrap under the input.

$Up to 20 characters18 characters left

A Counter written before the input still sits on the bottom row. Prefer writing it after the input so the count is in the server HTML.

Up to 80 characters

Past maxLength the count uses the error color. The shell stays as it is.

Up to 4 charactersCharacter limit reached

Without maxLength the count is just the number.

With maxLength, the Counter also tells screen readers the limit and, from 20 characters left, how many remain. That text comes from the characterLimit, charactersLeft and characterLimitReached strings of StringsProvider, English unless you set them.

Chip input

Controlled chips plus input. 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 htmlFor or Root's aria-label.

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.

Chip remove buttons draw a cross unless you pass chipRemoveIcon, as on Chip.

Deferred this slice: built-in clear, paste-split, clickable +N, chip arrow-key roving.

alphabeta

Optional useChipField holds chips and input; spread its getChipInputProps() onto Field.ChipInput.

commitKeys={['Enter', ',']}. Unknown keys are dropped.

one

maxRows={1} and {2} under compact and comfortable.

alphabetagammadeltaepsilonzetaetatheta
alphabetagammadeltaepsilonzetaetatheta
alphabetagammadeltaepsilonzetaetatheta
alphabetagammadeltaepsilonzetaetatheta

Disabled and invalid live on Field.Root.

locked
bad
alpha
bad

Row

A Field.Row is a full-width line. Write it before the input to sit above, or after to sit below. Put Field.Counter inside the Row when the field has a bottom row. Use autoGrow for a composer. Don't put bare text in a Row; wrap it in Field.Affix. Push trailing items to the end with style={{ marginInlineStart: 'auto' }} on the first of them. For chips, set chipLayout="row"; ChipInput draws its own chip line. Don't hand-roll chips in a Field.Row.

Up to 280 characters

The same composer at compact, comfortable, and spacious.

Up to 280 characters
Up to 280 characters
Up to 280 characters
alphabetagammadeltaepsilonzetaetatheta
tag
alphabetagammadeltaepsilonzetaetatheta
tag
alphabetagammadeltaepsilonzetaetatheta
tag
tag
tag

Number, date and time

Field.TextInput takes type="number", "date", "time" and "datetime-local". They stay native inputs, with the browser's own spinner, date picker and locale format, styled from the field tokens with figures lined up. value is the input's string: "2", "2026-10-07", "14:30", or an empty string while the input is incomplete. Constrain them with the native min, max and step.

Stepper buttons and a calendar grid are app compositions; build them around these inputs when the native ones are not enough.

Select

Field.Select is a native <select>. In browsers with appearance: base-select the button and its picker are drawn from field and option tokens; elsewhere the browser's own picker shows, with the same value and keyboard. There is no JavaScript listbox. Children are native <option> and <optgroup> elements, and a Label htmlFor names it like any other field.

<Label htmlFor={id}>Size</Label>
<Field.Root id={id}>
	<Field.Select value={size} onChange={(event) => setSize(event.target.value)}>
		<option value="s">Small</option>
		<option value="m">Medium</option>
	</Field.Select>
</Field.Root>

Custom controls

Your own control, such as a rating or a color well, joins a Field.Root through useField.

When to use

Field.Root

When: Shared field chrome for one text, textarea, or chip input — focus ring, disabled/invalid, density. Value lives on the input part.

When not: Selection controls (Checkbox / Radio / Switch). Overlay triggers that are buttons. Nesting another compound root inside.

Field.TextInput

When: Single-line typed value inside Field.Root (text / search / email / url / tel / password), or a native number, date or time.

When not: Multi-value tokens → Field.ChipInput. Multi-line → Field.Textarea. Standalone without Root.

Field.Textarea

When: Multi-line free text such as notes, messages or descriptions. rows sets the starting height, and autoGrow grows it up to maxRows with CSS only. When not: Single-line values, use Field.TextInput. Separate tags or tokens, use Field.ChipInput. Rich text or formatting, not shipped.

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.

Field.Select

When: Pick one value from a short, fixed list inside a form, with the native picker and keyboard.

When not: Filtering or searching a long list → Combobox recipe. Several values → Checkbox or Field.ChipInput. Commands rather than values → Menu.

Field.Icon

When: Leading/trailing icon or icon control inside Root. Position = DOM order. Decorative → interactive={false} (aria-hidden).

When not: Text adornments → Affix. Clickable clear/reveal as a dedicated Action (not shipped).

Field.Row

When: Group parts on their own full-width line inside the field, such as chips above a Field.ChipInput or attach, Field.Counter and send below a Field.Textarea.

When not: For a single Icon or Affix beside the input, compose it inline without a Row, and don't use a Row to lay out separate fields.

Field.Affix

When: Static text adornment (currency, unit) inside Root. Order = position.

When not: Icons → Icon. Interactive controls — Affix is not clickable.

Field.Counter

When: Show a live character count for a Field.TextInput or Field.Textarea, as n / max when maxLength is set and n otherwise. When not: For a hard limit with no visible count, set maxLength alone, and skip it on short fixed-length values like codes or PINs.

Props

Field.Root props
NameTypeRequiredDefaultDescription
disabledbooleannofalseDisables the input and marks the shell.
invalidbooleannofalseMarks the shell invalid and sets aria-invalid on the input.
idstringno—Input id. Auto-generated when omitted.
aria-describedbystringno—Ids of Description and ErrorMessage. Put this on Root. TextInput overwrites input-level aria.
namestringno—Native input name.
refRef<HTMLDivElement>no—Shell element. Use as a Popover anchorRef.
childrenReact.ReactNodeno—TextInput or ChipInput plus optional Icon and Affix. Order is leading/trailing.
Field.TextInput props
NameTypeRequiredDefaultDescription
valuestringyes—Controlled value.
onChange(event: ChangeEvent<HTMLInputElement>) => voidyes—Native change handler.
onValueChange(value: string) => voidno—Optional value callback.
placeholderstringno—Native placeholder.
type"text" | "search" | "email" | "url" | "tel" | "password" | "number" | "date" | "time" | "datetime-local"no"text"Native input type. Number, date and time keep the native spinner and picker, and value stays the input string, such as "2026-10-07".
Field.Textarea props
NameTypeRequiredDefaultDescription
valuestringyes—Controlled value.
onChange(event: ChangeEvent<HTMLTextAreaElement>) => voidyes—Native change handler.
onValueChange(value: string) => voidno—Optional value callback.
placeholderstringno—Native placeholder.
rowsnumberno"3"Starting height in rows. Also the native rows attribute and the drag floor.
maxRowsnumberno—Caps autoGrow and drag-resize. Omitted means no cap.
autoGrowbooleanno"false"Grows with content up to maxRows using CSS only. No maxRows means no cap.
Field.ChipInput props
NameTypeRequiredDefaultDescription
chipsstring[]yes—Controlled chip labels.
inputValuestringyes—Controlled input text.
onInputValueChange(value: string) => voidyes—Input text changes.
onCommit(value: string) => voidyes—Fires on a commit key with a trimmed non-empty value. Does not append chips.
onChipRemove(index: number, value: string) => voidyes—Remove button click or Backspace on an empty input.
chipRemoveLabel(value: string) => stringno—Remove button name. Default is the removeChip string from StringsProvider, Remove {value} in English.
chipRemoveIconReact.ReactNodeno—Chip remove button content, such as an icon from your own set. Without it, the CSS draws a cross.
overflowLabel(n: number) => stringno—+N name. Default is the moreChips string from StringsProvider, {n} more in English.
commitKeysReadonlyArray<"Enter" | "Tab" | "," | ";" | " ">no["Enter"]Closed allowlist of commit keys.
maxRowsnumberno—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"no"inline"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.
removeOnBackspacebooleannotrueBackspace on an empty input removes the last chip.
placeholderstringno—Native placeholder.
Field.Select props
NameTypeRequiredDefaultDescription
valuestringyes—Controlled value.
onChange(event: ChangeEvent<HTMLSelectElement>) => voidno—Native change handler. Optional when onValueChange is enough.
onValueChange(value: string) => voidno—Optional value callback.
childrenReact.ReactNodeno—Native <option> and <optgroup> elements.
classNamestringno—Optional class on the select.
Field.Icon props
NameTypeRequiredDefaultDescription
childrenReact.ReactNodeno—Icon or control.
interactivebooleannofalseWhen false, the wrapper is aria-hidden.
Field.Affix props
NameTypeRequiredDefaultDescription
childrenReact.ReactNodeno—Affix content. Strings render as text.
Field.Row props
NameTypeRequiredDefaultDescription
childrenReact.ReactNodeno—Parts for the line, such as chips, buttons or Field.Counter.
classNamestringno—Extra class on the row.
Field.Counter props
NameTypeRequiredDefaultDescription
classNamestringno—Class on the count. Use it for the inline flex-basis override.
styleReact.CSSPropertiesno—Inline style. flex-basis: auto places the count on the input row.