# Input Components

> Generated reference (regen-owned; see [index.md](index.md)). Binds only under `primitives: nurix`.

## Shared Props

Every Control takes one value vocabulary, so a host (a `Cell`, a field grid, a table cell) binds any of them the same way:

| Prop | Type | Description |
| --- | --- | --- |
| `value` / `defaultValue` | `T` | Controlled value or uncontrolled seed; binary Controls use `checked` (Checkbox, Switch) or `pressed` (Toggle) |
| `onValueChange` | `(value, details) => void` | Every change, value first; `details` is Base UI's event details, and `details.cancel()` vetoes a discrete change |
| `onValueCommitted` | `(value) => void` | The one commit channel: once per commit, never for an unchanged value. Typed fields commit on Enter (Cmd/Ctrl+Enter when multi-line) or blur and revert on Escape; discrete Controls commit in the same tick as `onValueChange` |
| `validate` | `(value) => string \| null \| Promise<string \| null>` | A message refuses the commit, keeps the draft and sets `aria-invalid` |
| `disabled`, `readOnly`, `required`, `name` | `boolean`, `string` | Field state; `required` removes the automatic Clear on Select, DatePicker and IconPicker, `name` submits through a hidden input |
| `variant="bare"`, `size` | `bare`, `xs\|sm\|default\|lg` | `bare` drops the frame, focus ring and invalid paint a surface underneath already draws; a host supplies both through host context |

Each value-holding Control also exports a `format` static (`Input.format`, `Select.format`, `DatePicker.format` …) that writes the at-rest text a host shows.

## Cell

Form-field host: label, control, addons, helper and error in one block, with field state set once on the root (`@nurix/components/compound/cell`, plus `useCellContext`).

**Props:** `mode` [default|inline], `size` [xs|sm|default|lg], `orientation` [vertical|horizontal], `disabled`, `readOnly`, `required`, `invalid`

**Sub-components:** `Cell.Label`, `Cell.Content`, `Cell.Field` (`type` [text|textarea|number|checkbox|switch|rating|slider|select], `value`, `onValueCommitted`, `options`, `format`), `Cell.Prefix`, `Cell.Suffix`, `Cell.Stepper`, `Cell.Helper`, `Cell.Error`

`Cell.Error` renders only while the root's `invalid` is true; `Cell.Stepper` drives a hosted NumberInput; `disabled` makes the whole `Cell.Content` inert, Prefix/Suffix buttons included. `mode="inline"` shows text-like types as text until clicked, while marks (checkbox, switch, rating) stay live in place.

```tsx
<Cell required invalid={!!seatsError}>
  <Cell.Label>Seats</Cell.Label>
  <Cell.Content>
    <Cell.Field type="number" value={seats} onValueCommitted={setSeats} options={{ min: 0, max: 20 }} />
    <Cell.Suffix>seats</Cell.Suffix>
    <Cell.Stepper />
  </Cell.Content>
  <Cell.Helper>Whole seats, 0 to 20</Cell.Helper>
  <Cell.Error>{seatsError}</Cell.Error>
</Cell>
```

## Input

Single-line text field; `onValueCommitted` turns on the local draft, Enter/blur commits, Escape reverts (`@nurix/components/element/input`).

**Props:** `onValueChange`, `onValueCommitted`, `validate`, `prefix`, `suffix` (decorative glyphs only), `size`, `variant` [default|bare]

Without `onValueCommitted`, `validate` never runs and a controlled `value` reads straight through. Labels, hints and interactive affixes belong to `Cell`.

```tsx
<Input
  value={name}
  onValueChange={setName}
  onValueCommitted={saveName}
  validate={(v) => (v.trim() ? null : "Name is required")}
  placeholder="Enter name"
/>
```

## NumberInput

Typed, bounded number; commits on Enter, blur and each arrow or step press, Escape reverts (`@nurix/components/element/number-input`).

**Props:** `min`, `max`, `step`, `format` (Intl options), `locale`, `onValueCommitted`, `validate`, `size`, `variant` [default|bare]

An empty field commits `null`, not `0`. No negatives is `min={0}`; whole numbers are `format={{ maximumFractionDigits: 0 }}`. Never `Input type="number"`.

```tsx
<NumberInput value={count} onValueCommitted={setCount} min={0} max={100} step={1} />
```

## Textarea

Multi-line field; commits on Cmd/Ctrl+Enter or blur (bare Enter is a newline), Escape reverts (`@nurix/components/element/textarea`).

**Props:** `onValueChange`, `onValueCommitted`, `validate`, `rows` (default 3), `autosize` [boolean|{ maxRows }], `variant` [default|bare]

```tsx
<Textarea
  value={description}
  onValueChange={setDescription}
  onValueCommitted={saveDescription}
  autosize={{ maxRows: 8 }}
/>
```

## Checkbox

Boolean box on Base UI `Checkbox.Root` (`@nurix/components/element/checkbox`).

**Props:** `checked`, `defaultChecked`, `onCheckedChange`, `onValueCommitted`, `indeterminate`, `size` [sm|default], `variant` [default|bare]

`indeterminate` is the caller's to clear, and while set the box emits only `data-indeterminate`, never `data-checked`. `value` is the string a form posts, not the checked state.

```tsx
<Checkbox checked={agreed} onCheckedChange={setAgreed} />
```

## Switch

On/off setting that applies the moment it flips; ships no label (`@nurix/components/element/switch`).

**Props:** `checked`, `defaultChecked`, `onCheckedChange`, `onValueCommitted`, `size` [sm|default], `variant` [default|bare]

There is no `onChange`, and `value` is the submitted string, never a boolean. Name it with `aria-label` or wrap it in `Cell`.

```tsx
<Switch checked={enabled} onCheckedChange={setEnabled} aria-label="Notifications" />
```

## Select

Closed listbox picking one string value, with no query field (`@nurix/components/element/select`).

**Props:** `items` (required; `{ value: label }` map or `{ value, label }[]`), `value`, `defaultValue`, `onValueChange`, `onValueCommitted`, `validate`, `required`, `variant` [default|bare], `labels` (`clear`)

**Sub-components:** `Select.Trigger` (`size` [xs|sm|default|lg]), `Select.Value` (`placeholder`), `Select.Popup` (`side`, `align`, `sideOffset`, `alignItemWithTrigger`), `Select.Group`, `Select.Label`, `Select.Item`, `Select.Separator`

Values are strings only. A non-required Select with a value shows a Clear row that commits `null`. Search, async loading, multi-select or a long list is `Combobox`.

```tsx
<Select items={statusItems} value={status} onValueChange={setStatus}>
  <Select.Trigger>
    <Select.Value placeholder="Status" />
  </Select.Trigger>
  <Select.Popup>
    <Select.Item value="active">Active</Select.Item>
    <Select.Item value="paused">Paused</Select.Item>
  </Select.Popup>
</Select>
```

## Combobox

Searchable option picker: single or multi, client or remote filtering, paging (`@nurix/components/compound/combobox`, plus `useComboboxContext`).

**Props:** `options` (`{ value, label }[]`), `value`, `onValueChange(value, { option })`, `onValueCommitted`, `multiple`, `max`, `isClearable`, `searchValue`, `onSearchValueChange`, `loading`, `hasMore`, `loadingMore`, `onLoadMore`, `selectionIndicator` [checkbox|check-end|bg-only], `variant` [default|bare], `labels`

**Sub-components:** `Combobox.Trigger` (`display` [tags|comma|count], `size` [sm|default]), `Combobox.Popup` (`maxHeight`), `Combobox.Search`, `Combobox.List`, `Combobox.Item`, `Combobox.Loading`, `Combobox.Empty`, `Combobox.SelectAll`

Holding the query (`onSearchValueChange`) turns the built-in filter off. Multi mode commits once on close, and Escape reverts. The placeholder never names the trigger, so pass `aria-label`.

```tsx
<Combobox options={owners} value={owner} onValueChange={setOwner}>
  <Combobox.Trigger aria-label="Owner" />
  <Combobox.Popup>
    <Combobox.Search />
    <Combobox.List>
      {owners.map((o) => <Combobox.Item key={o.value} value={o.value}>{o.label}</Combobox.Item>)}
    </Combobox.List>
    <Combobox.Empty />
  </Combobox.Popup>
</Combobox>
```

## DatePicker

Date field with a calendar popover; the value is a `Date`, an ISO string or `null` (`@nurix/components/compound/date-picker`).

**Props:** `onValueChange`, `onValueCommitted`, `validate`, `placeholder`, `formatPattern`, `isRelative`, `hasIcon`, `hasPresets`, `presets`, `captionLayout` [label|dropdown|dropdown-months|dropdown-years], `numberOfMonths`, `showOutsideDays`, `min`, `max`, `weekStartsOn`, `required`, `name`, `size`, `variant` [default|bare]

A non-required picker shows a Clear action in its popup. `name` submits local `yyyy-MM-dd`. `DatePicker.format(value)` is the at-rest text.

```tsx
<DatePicker
  value={date}
  onValueChange={setDate}
  onValueCommitted={commitDate}
  placeholder="Choose date"
/>
```

## DatePicker.Range

Date-window picker on the same namespace; the value is `{ from: Date, to: Date } | null` (`DateRangeValue`).

**Props:** the DatePicker props, with `presets` as range presets (`DateRangePreset[]`)

A window commits on the second grid press; closing after one press drops it without a commit. `DatePicker.Range.format(value)` is the at-rest text.

```tsx
<DatePicker.Range value={range} onValueChange={setRange} onValueCommitted={commitRange} hasPresets />
```

## Slider

Range control for one number between `min` and `max` (`@nurix/components/element/slider`).

**Props:** `min`, `max`, `step`, `onValueChange`, `onValueCommitted`, `hasValueReadout`, `hasBoundsReadout`, `format` (Intl options), `locale`, `inputRef`, `variant` [default|bare]

`value` is always a single number. `onValueCommitted` fires once per release or key press that changes the value, and Escape reverts an uncommitted drag.

```tsx
<Slider value={volume} onValueChange={setVolume} onValueCommitted={saveVolume} min={0} max={100} aria-label="Volume" />
```

## Rating

Icon score picker (`@nurix/components/element/rating`, plus `RATING_ICONS`).

**Props:** `max`, `step` [1|0.5], `size` [xs|sm|default|lg], `icon` [Star|Heart|ThumbsUp|Circle|Flame|Zap|Trophy|Diamond], `hasValueText`, `onValueCommitted`, `validate`, `name`, `variant` [default|bare], `labels`

A controlled `value` moves no star until the parent updates it. No click brings a set score below one `step`, so clearing means setting `value` to `0`. It ships no group name.

```tsx
<Rating value={score} onValueChange={setScore} max={5} step={0.5} icon="Heart" aria-label="Score" />
```

## Button

Action button; the element changes through `render`, never by wrapping (`@nurix/components/element/button`, plus `buttonVariants`).

**Props:** `variant` [default|secondary|outline|ghost|link], `intent` [neutral|danger], `size` [xs|sm|default|lg], `isIconOnly`, `loading`, `pressed`, `focusableWhenDisabled`, `render`, `labels` (`pending`)

An irreversible action takes `intent="danger"` on any variant. An icon-only button adds `isIconOnly` plus an `aria-label`. `loading` keeps focus and width; `pressed` is a controlled latch rendering `aria-pressed`. Inside a host, omit `size`.

```tsx
<Button variant="outline" intent="danger" loading={isDeleting} onClick={handleDelete}>
  Delete
</Button>
```

## Calendar

Inline month grid on `react-day-picker` (`@nurix/components/compound/calendar`); a form field wants `DatePicker` instead.

**Props:** `value`, `onValueChange`, `onValueCommitted`, `multiple`, `min`, `max`, `disabledDates` (react-day-picker matchers), `disabled`, `readOnly`, `required`, `name`, `validate`, `captionLayout`, `numberOfMonths`

**Sub-components:** `Calendar.Range` (value `{ from, to? } | null`, `excludeDisabled`, `resetOnSelect`), `Calendar.DayButton`

Re-pressing the selected day clears to `null` unless `required`. `Calendar.Range` commits only a complete or cleared range. Returned dates are local midnight.

```tsx
<Calendar value={date} onValueChange={setDate} captionLayout="dropdown" min={today} />
```

## IconPicker

Searchable lucide icon picker popover over caller-loaded records (`@nurix/components/compound/icon-picker`; optional peers `@tanstack/react-virtual` and `fuse.js`).

**Props:** `data` (required; `iconsData` from `lib/icons-data`, loaded lazily), `loading`, `value` (`IconName | null`, kebab-case), `onValueChange`, `onValueCommitted`, `isSearchable`, `isCategorized`, `modal`, `render`, `required`, `size`, `variant` [default|bare], `labels`

A non-required picker carries an automatic Clear. Draw the pick with `Icon` from `element/icon`.

```tsx
<IconPicker data={icons} loading={isLoading} value={icon} onValueChange={setIcon} />
```

## InputOTP

Segmented one-time-code field on Base UI's OTP field (`@nurix/components/compound/input-otp`).

**Props:** `length` (must equal the slot count), `value`, `onValueChange`, `onValueCommitted` (fires when the last slot fills), `status` [pending|error|success], `mask`, `autoSubmit`, `labels`

**Sub-components:** `InputOTP.Group`, `InputOTP.Slot`, `InputOTP.Separator`, `InputOTP.Check`

`status` is the caller's verdict: the field never decides a code is wrong.

```tsx
<InputOTP length={4} value={code} onValueChange={setCode} onValueCommitted={verify} status={status}>
  <InputOTP.Group>
    <InputOTP.Slot /><InputOTP.Slot /><InputOTP.Slot /><InputOTP.Slot />
  </InputOTP.Group>
  <InputOTP.Check />
</InputOTP>
```

## FilePicker

Acquires and stages local files through a dropzone and a picker button (`@nurix/components/compound/file-picker`, plus `useFilePicker`).

**Props:** `value` (`File[]`), `onValueChange`, `onValueCommitted`, `multiple`, `accept`, `maxFiles`, `maxSize`, `validate(file)`, `onFileReject`, `onUpload`, `readOnly`, `labels`

**Sub-components:** `FilePicker.Dropzone`, `FilePicker.Trigger`, `FilePicker.List`, `FilePicker.Item` (`value` = the File), `FilePicker.ItemPreview`, `FilePicker.ItemMetadata`, `FilePicker.ItemProgress`, `FilePicker.ItemDelete`, `FilePicker.Clear`

The dropzone is a non-interactive region, so it always holds a `FilePicker.Trigger`. Supply `onUpload` only when the product owns a transport. An existing attachment is `FilePreview` (`compound/file-preview`).

```tsx
<FilePicker value={files} onValueChange={setFiles} multiple maxFiles={5}>
  <FilePicker.Dropzone>
    <FilePicker.Trigger>Choose files</FilePicker.Trigger>
  </FilePicker.Dropzone>
  <FilePicker.List>
    {files.map((file) => <FilePicker.Item key={file.name} value={file}><FilePicker.ItemMetadata /><FilePicker.ItemDelete>Remove</FilePicker.ItemDelete></FilePicker.Item>)}
  </FilePicker.List>
</FilePicker>
```

## RadioGroup

Small set of mutually exclusive options, as radio marks or whole-card radios (`@nurix/components/element/radio-group`).

**Props:** `value`, `defaultValue`, `onValueChange`, `onValueCommitted`, `name` (on the root, never an item), `orientation` [vertical|horizontal], `variant` [default|bare]

**Sub-components:** `RadioGroup.Item` (`size` [sm|default]), `RadioGroup.Card` (`variant` [default|list]), `RadioGroup.Header`, `RadioGroup.Title`, `RadioGroup.Description`, `RadioGroup.Indicator`

An `Item`'s `id` lands on its hidden input, so aim the `Label`'s `htmlFor` (`element/label`) at it. The segmented control is `ToggleGroup`.

```tsx
<RadioGroup value={plan} onValueChange={setPlan} aria-label="Plan">
  <RadioGroup.Item value="free" id="plan-free" />
  <Label htmlFor="plan-free">Free</Label>
  <RadioGroup.Item value="pro" id="plan-pro" />
  <Label htmlFor="plan-pro">Pro</Label>
</RadioGroup>
```

## Toggle

Single control that holds its own on/off state (`@nurix/components/element/toggle`, plus `toggleVariants`).

**Props:** `pressed`, `defaultPressed`, `onPressedChange`, `onValueCommitted`, `variant` [ghost|outline|default|bare] (default `ghost`), `size` [sm|default|lg], `isIconOnly`

`value` and `onChange` are not boolean aliases. Style off `data-pressed`.

```tsx
<Toggle pressed={isBold} onPressedChange={setIsBold} isIconOnly aria-label="Toggle bold">
  <Bold />
</Toggle>
```

## ToggleGroup

Strip of toggles picking one value, or several with `multiple` (`@nurix/components/compound/toggle-group`).

**Props:** `multiple`, `value`, `defaultValue`, `onValueChange`, `onValueCommitted`, `variant` [ghost|outline|default|bare], `size` [sm|default|lg], `orientation`

**Sub-components:** `ToggleGroup.Item` (takes no `variant` or `size`; both cascade from the group)

Single mode publishes `""` when the pressed item is toggled off, so guard it when a selection is mandatory.

```tsx
<ToggleGroup variant="outline" value={view} onValueChange={(next) => next && setView(next)}>
  <ToggleGroup.Item value="table">Table</ToggleGroup.Item>
  <ToggleGroup.Item value="card">Card</ToggleGroup.Item>
</ToggleGroup>
```

## ThemeToggle

Light/dark toggle (`@nurix/components/element/theme-toggle`) with a circular view-transition reveal from the button; it is the library `Toggle`, with `pressed` meaning dark.

**Props:** `pressed` + `onPressedChange` (pass both when the app owns theme state; omitted, it reads and writes the `.dark` class on `document.documentElement`), `onValueCommitted`, `labels` (`{ name }`, default "Dark theme"), plus Toggle's `variant` [ghost|outline|default|bare] and `size` [sm|default|lg]. It persists nothing — store the choice yourself; the reveal is timed by the motion tokens (the `pace` trait), with no `duration` prop.

```tsx
<ThemeToggle pressed={theme === "dark"} onPressedChange={(isDark) => setTheme(isDark ? "dark" : "light")} />
```

## InlineEdit

Click-to-edit single-line text: one `<span contentEditable>` that reads as text until focused (`@nurix/components/element/inline-edit`).

**Props:** `value`, `defaultValue`, `placeholder`, `onSave`, `validator` (RegExp), `onValidationError`, `hideHoverEffect`, `readOnly`

Enter or blur commits, Escape reverts, and unchanged text skips `onSave`. A labelled field that edits in place is `Cell mode="inline"`.

```tsx
<InlineEdit value={title} onSave={setTitle} placeholder="Click to add a title" />
```

## Reaction

Strip of toggleable emoji chips with counts (`@nurix/components/compound/reaction`, plus `reactionChipVariants`).

**Props:** `data` (`{ emoji, count? }[]`, sets order and counts), `value` (pressed emoji), `onValueChange(next, { emoji, pressed })`, `onValueCommitted`, `maxVisible`, `size` [xs|sm], `readOnly`, `variant` [default|bare], `labels`

**Sub-components:** `Reaction.Chip`, `Reaction.Add`

Counts render exactly as `data` gives them, so an optimistic UI steps the count itself. `Reaction.Add` is an empty shell, so give it the glyph.

```tsx
<Reaction data={counts} value={pressed} onValueChange={(next, { emoji, pressed: isOn }) => { setPressed(next); write(emoji, isOn); }} aria-label="Reactions">
  <Reaction.Add aria-label="Add reaction">
    <SmilePlus className="size-4" />
  </Reaction.Add>
</Reaction>
```

## ActionCluster

Record-header action row: up to four inline buttons, pinned actions after a divider, the rest in a "More actions" dropdown (`@nurix/components/compound/action-cluster`).

**Props:** `actions` (`ClusterAction<TData>[]`: `label`, `id`, `icon`, `variant`, `intent`, `pinned`, `disabled`, `loading`, `onClick(data, event)`), `data` (required), `size` (default `sm`), `labels` (`group`, `more`)

A plain row of buttons is `Row` with `Button`s; hover-revealed icon actions are `ActionBar`.

```tsx
<ActionCluster
  data={account}
  actions={[
    { label: "Edit", icon: <Pencil />, onClick: openEditor },
    { label: "Delete", intent: "danger", pinned: true, onClick: confirmDelete },
  ]}
/>
```

## TiptapEditor

Rich-text editor, markdown in and markdown out (`@nurix/components/block/tiptap-editor`, plus `NurixTiptapKit`, `getMarkdown`, `blockMarkdown`; import `@nurix/components/tiptap-editor.css` once; optional peers `@tiptap/*` and `tiptap-markdown`).

**Props:** `value`, `defaultValue`, `onValueChange`, `onValueCommitted` (Cmd/Ctrl+Enter or blur), `validate`, `readOnly`, `disabled`, `name`, `extensions`, `toolbar`, `tableControls`, `variant` [default|bare], `labels`

**Sub-components:** `TiptapEditor.Toolbar`, `TiptapEditor.TableControls`, `TiptapEditor.TableGridPicker`

A controlled `value` is safe, because the editor ignores an echo of its own last emission. Read markdown through `getMarkdown(editor)`, never `editor.storage.markdown`.

```tsx
<div className="h-96">
  <TiptapEditor defaultValue={doc} onValueCommitted={saveNotes} aria-label="Notes" />
</div>
```

## Fallback-only concepts

No nurix primitive — build each from its plain-spec fallback in [index.md](index.md):

- `form-validation`
- `code-editor`
