# Number Field (/ui/components/react/number-field)



<Playground component="number-field" />

## Usage [#usage]

```tsx
import { NumberField } from '@appica/ui-react/number-field'
```

```tsx
<NumberField defaultValue={1} min={0} max={10} />
```

`NumberField` folds the decrement button, text input, and increment button into one control — you render a single `<NumberField />`, not a set of parts. It accepts only numeric input, steps with the buttons or the ↑/↓ arrow keys, and reformats the value when you commit. Use it uncontrolled with `defaultValue`, or controlled with `value` + `onValueChange`.

Unlike a checkbox or radio, the input is a real text field, so give it a label the usual way — an external `<label htmlFor>` pointing at the field's `id`, or an `aria-label`. The rolling-digit animation on stepper presses is skipped when the user prefers reduced motion.

## Examples [#examples]

### Variants [#variants]

`variant` switches the appearance between a bordered `outline` (default) and a filled `soft` field. The stepper buttons invert their fill so they stay legible against either.

```tsx
import { NumberField } from '@appica/ui-react/number-field'

export default function NumberFieldVariants() {
  return (
    <div className="flex flex-wrap items-center gap-6">
      <NumberField variant="outline" defaultValue={3} aria-label="Outline quantity" />
      <NumberField variant="soft" defaultValue={3} aria-label="Soft quantity" />
    </div>
  )
}
```

### Sizes [#sizes]

`size` scales the field width, steppers, and text together — `sm`, `md` (default), or `lg`.

```tsx
import { NumberField } from '@appica/ui-react/number-field'

export default function NumberFieldSizes() {
  return (
    <div className="flex flex-wrap items-center gap-6">
      <NumberField size="sm" defaultValue={3} aria-label="Small quantity" />
      <NumberField size="md" defaultValue={3} aria-label="Medium quantity" />
      <NumberField size="lg" defaultValue={3} aria-label="Large quantity" />
    </div>
  )
}
```

### Min, max & step [#min-max--step]

`min` and `max` clamp the value and disable the stepper once a bound is reached; clearing the field and blurring snaps back to `min` (or `0`). `step` sets how far each press or arrow key moves the value — hold **Shift** for `largeStep` (10× by default) or **Alt** for `smallStep`.

```tsx
import { Field, FieldLabel } from '@appica/ui-react/field'
import { NumberField } from '@appica/ui-react/number-field'

export default function NumberFieldMinMaxStep() {
  return (
    <Field>
      <FieldLabel className="mb-2">Rating (0-20, step 1)</FieldLabel>
      <NumberField defaultValue={5} min={0} max={20} step={1} />
    </Field>
  )
}
```

### Formatting [#formatting]

Pass `format` — an `Intl.NumberFormatOptions` object — and an optional `locale` to display the value as currency, a percentage, units, and the like. The display reformats while the underlying value stays a plain number.

```tsx
import { NumberField } from '@appica/ui-react/number-field'

export default function NumberFieldFormatting() {
  return (
    <div className="flex flex-wrap items-end gap-6">
      <div className="flex flex-col gap-1.5">
        <label htmlFor="nf-price" className="text-foreground-intense text-sm font-medium">
          Price
        </label>
        <NumberField
          id="nf-price"
          className="w-40"
          defaultValue={19.99}
          step={0.5}
          format={{ style: 'currency', currency: 'USD' }}
          locale="en-US"
        />
      </div>
      <div className="flex flex-col gap-1.5">
        <label htmlFor="nf-rate" className="text-foreground-intense text-sm font-medium">
          Rate
        </label>
        <NumberField
          id="nf-rate"
          className="w-32"
          defaultValue={0.15}
          min={0}
          max={1}
          step={0.01}
          format={{ style: 'percent' }}
        />
      </div>
    </div>
  )
}
```

### Controlled [#controlled]

Drive the value yourself with `value` + `onValueChange`. The handler receives `null` when the field is empty, so coalesce before using the number.

```tsx
'use client'

import { useState } from 'react'
import { NumberField } from '@appica/ui-react/number-field'

export default function NumberFieldControlled() {
  const [value, setValue] = useState<number | null>(2)
  return (
    <div className="flex flex-col items-center gap-3">
      <NumberField value={value} onValueChange={setValue} min={0} aria-label="Guests" />
      <p className="text-sm">
        {value ?? 0} {value === 1 ? 'guest' : 'guests'}
      </p>
    </div>
  )
}
```

### Disabled & error states [#disabled--error-states]

`disabled` greys out the field and both steppers and blocks interaction. Set `aria-invalid` to paint the error state — it's mirrored to `data-invalid` on the field and its steppers. Wrapping the field in a [`Field`](/ui/components/react/field) marked `invalid` produces the same styling and is the better choice when you're also rendering a validation message.

```tsx
import { NumberField } from '@appica/ui-react/number-field'

export default function NumberFieldStates() {
  return (
    <div className="flex flex-wrap items-center gap-6">
      <NumberField variant="soft" defaultValue={3} disabled aria-label="Disabled quantity" />
      <NumberField variant="soft" defaultValue={3} aria-invalid aria-label="Quantity with error" />
    </div>
  )
}
```

## RTL [#rtl]

Every Appica UI component supports right-to-left layouts out of the box. Set the `dir` attribute on a container (commonly your `<html>` element) so CSS logical properties resolve correctly, and wrap your tree in `DirectionProvider` so direction-aware behavior — roving focus, popup placement, and the like — follows the same direction.

```tsx
import { DirectionProvider } from '@appica/ui-react/providers/direction-provider'

export default function RootLayout({ children }) {
  return (
    <html lang="ar" dir="rtl">
      <body>
        <DirectionProvider dir="rtl">{children}</DirectionProvider>
      </body>
    </html>
  )
}
```

The decrement and increment steppers swap sides to follow the resolved direction, and a matching `locale` formats the value with that locale's own digits. For setup details and caveats, see the [RTL guide](/ui/docs/react/rtl).

<RtlPreview component="number-field" />

## API reference [#api-reference]

A single control. The `variant`, `size`, `placeholder`, and `inputProps` props are Appica additions; every other prop is forwarded to [Base UI's NumberField.Root](https://base-ui.com/react/components/number-field).

| Prop               | <ColMinWidth width="200">Type</ColMinWidth>           | Default     | <ColMinWidth width="220">Description</ColMinWidth>                                     |
| ------------------ | ----------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------- |
| `variant`          | <Code>'outline' \| 'soft'</Code>                      | `'outline'` | Field appearance — bordered or filled.                                                 |
| `size`             | <Code>'sm' \| 'md' \| 'lg'</Code>                     | `'md'`      | Scales field width, steppers, and text.                                                |
| `value`            | <Code>number \| null</Code>                           | —           | Controlled value (`null` when empty). Pair with `onValueChange`.                       |
| `defaultValue`     | `number`                                              | —           | Uncontrolled initial value.                                                            |
| `onValueChange`    | <Code>(value: number \| null, details) => void</Code> | —           | Fires as the value changes — typing, stepping, or scrubbing.                           |
| `onValueCommitted` | <Code>(value: number \| null, details) => void</Code> | —           | Fires when the value is finalized (blur, Enter, stepper release).                      |
| `min`              | `number`                                              | —           | Minimum allowed value; the field snaps here when cleared.                              |
| `max`              | `number`                                              | —           | Maximum allowed value.                                                                 |
| `step`             | <Code>number \| 'any'</Code>                          | `1`         | Amount each stepper press or arrow key moves the value.                                |
| `smallStep`        | `number`                                              | `0.1`       | Step taken while **Alt** is held.                                                      |
| `largeStep`        | `number`                                              | `10`        | Step taken while **Shift** is held.                                                    |
| `snapOnStep`       | `boolean`                                             | `false`     | Snap to the nearest multiple of `step` when stepping.                                  |
| `format`           | `Intl.NumberFormatOptions`                            | —           | Formatting options (currency, percent, units, …) for the displayed value.              |
| `locale`           | `Intl.LocalesArgument`                                | —           | Locale used to format and parse the value.                                             |
| `allowWheelScrub`  | `boolean`                                             | `false`     | Allow adjusting the value by scrolling the mouse wheel over the field.                 |
| `placeholder`      | `string`                                              | `' '`       | Placeholder text shown when empty.                                                     |
| `inputProps`       | <Code>ComponentProps\<'input'></Code>                 | —           | Props forwarded to the inner `<input>` (`name`, `onChange`, `aria-*`, `className`, …). |
| `name`             | `string`                                              | —           | Field name submitted with a form.                                                      |
| `disabled`         | `boolean`                                             | `false`     | Disables the input and both stepper buttons.                                           |
| `readOnly`         | `boolean`                                             | `false`     | Focusable but the value can't be changed.                                              |
| `required`         | `boolean`                                             | `false`     | Must have a value before its form submits.                                             |
| `aria-invalid`     | <Code>boolean \| 'true' \| 'false'</Code>             | —           | Marks the error state; mirrored to `data-invalid` on the field and steppers.           |
| `inputRef`         | <Code>Ref\<HTMLInputElement></Code>                   | —           | Ref to the inner `<input>` element.                                                    |
| `id`               | `string`                                              | —           | `id` of the inner `<input>` — point an external `<label htmlFor>` at it.               |
| `className`        | `string`                                              | —           | Extra classes on the field root, merged via `tailwind-merge`.                          |

`NumberField` wraps Base UI's `NumberField` (`Root`, `Input`, `Increment`, `Decrement`) and forwards every remaining Root prop. It exposes `data-*` state attributes (`data-disabled`, `data-readonly`, `data-invalid`, `data-scrubbing`, …) on the field and its parts for styling — see the Base UI docs for the full surface. When placed inside a Base UI `Field.Root` marked `invalid`, the error state (`data-invalid`) propagates to the input and both steppers automatically.

## Accessibility [#accessibility]

* The inner element is a real `<input>` with `role="spinbutton"`, exposing `aria-valuenow`, `aria-valuemin`, and `aria-valuemax` to assistive tech.
* Give the field an accessible name — an external `<label htmlFor>` pointing at its `id`, or an `aria-label`.
* The steppers are real `<button>`s labelled **Increase value** / **Decrease value**, and become `disabled` at a bound or when the field is disabled.
* **↑/↓** step the value (with **Shift**/**Alt** for the large/small step), and typing accepts only valid numeric input.
* `disabled` blocks all interaction; `readOnly` keeps the field focusable but inert.
* `aria-invalid` is bridged to `data-invalid` so the error state can be styled without a wrapping `Field`.
* The rolling-digit animation on stepper presses honours `prefers-reduced-motion`.
