# Number Field (/ui/components/react/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` grays 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).


## 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               | Type                   | Default     | Description                                           |
| ------------------ | ------------------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------- |
| `variant`          | `'outline' \| 'soft'`                              | `'outline'` | Field appearance - bordered or filled.                                                       |
| `size`             | `'sm' \| 'md' \| 'lg'`                             | `'md'`      | Scales field width, steppers, and text.                                                      |
| `value`            | `number \| null`                                   | -           | Controlled value (`null` when empty). Pair with `onValueChange`.                             |
| `defaultValue`     | `number`                                                      | -           | Uncontrolled initial value.                                                                  |
| `onValueChange`    | `(value: number \| null, details) => void`         | -           | Fires as the value changes - typing, stepping, or scrubbing.                                 |
| `onValueCommitted` | `(value: number \| null, details) => void`         | -           | 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`             | `number \| 'any'`                                  | `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`       | `ComponentProps\<'input'>`                         | -           | 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`     | `boolean \| 'true' \| 'false'`                     | -           | Marks the error state; mirrored to `data-invalid` on the field and steppers.                 |
| `inputRef`         | `Ref\<HTMLInputElement>`                           | -           | Ref to the inner `<input>` element.                                                          |
| `id`               | `string`                                                      | -           | `id` of the inner `<input>` - point an external `<label htmlFor>` at it.                     |
| `render`           | `ReactElement \| ((props, state) => ReactElement)` | -           | Replace the underlying element, or compose it with another component.                        |
| `allowOutOfRange`  | `boolean`                                                     | `false`     | Allow typed values outside `min`/`max` without clamping, so native validation can flag them. |
| `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 labeled **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 honors `prefers-reduced-motion`.
