# Number Input

A text field for a number, with keyboard stepping, optional increment and decrement buttons, and locale-aware formatting.

```tsx
import { NumberInput } from "@zenoxai/ui/components/number-input"

function NumberInputDemo() {
  return (
    <NumberInput
      defaultValue={5}
      min={1}
      max={50}
      ariaLabel="Failed logins before lockout"
    />
  )
}
```

## Usage

```tsx
import { NumberInput } from "@zenoxai/ui/components/number-input"

<NumberInput defaultValue={5} min={1} max={50} ariaLabel="Failed logins before lockout" />
```

`NumberInput` is a single boxed input on Base UI's Number Field. It accepts the Number Field
root props: `value`, `defaultValue`, `onValueChange`, `min`, `max`, `step`, `format`, `name`,
`required`, `disabled`, and `size`. Three props of its own reach the inner `<input>`:
`ariaLabel`, `inputClassName` and `inputRef`. `className` and any other attribute go to the
wrapper. The field has the height, padding, fill, focus ring and disabled look of an
[Input](/components/input).

For stepper buttons, compose the parts instead: `NumberInputRoot`, `NumberInputGroup`,
`NumberInputDecrement`, `NumberInputField` and `NumberInputIncrement`.

Use it for counts, thresholds, ports and durations: values the user adjusts by small amounts
or types exactly. For a value picked roughly from a range, use a [Slider](/components/slider).
For numbers that are identifiers (a ZIP code, an account number), use an
[Input](/components/input): they are not quantities and must not be stepped or formatted.

## Examples

### With a label

`id` on `NumberInput` goes to the `<input>`, so a `FieldLabel` from
[Field](/components/field) can point at it. The input is 80px wide; widen it with
`inputClassName` when the formatted value is longer.

```tsx
import { Field, FieldDescription, FieldLabel } from "@zenoxai/ui/components/field"
import { NumberInput } from "@zenoxai/ui/components/number-input"

function NumberInputWithLabel() {
  return (
    <Field className="w-fit">
      <FieldLabel htmlFor="retention-days">Retention</FieldLabel>
      <NumberInput
        id="retention-days"
        defaultValue={90}
        min={7}
        max={365}
        format={{ style: "unit", unit: "day", unitDisplay: "long" }}
        inputClassName="w-28"
      />
      <FieldDescription>Events older than this are deleted.</FieldDescription>
    </Field>
  )
}
```

### With buttons

`NumberInputGroup` draws one border around the buttons and the field. The buttons are named
"Decrease" and "Increase"; pass `aria-label` to translate them. Holding a button repeats the
step.

```tsx
import { NumberInputDecrement, NumberInputField, NumberInputGroup, NumberInputIncrement, NumberInputRoot } from "@zenoxai/ui/components/number-input"

function NumberInputWithButtons() {
  return (
    <NumberInputRoot defaultValue={3} min={1} max={10}>
      <NumberInputGroup>
        <NumberInputDecrement />
        <NumberInputField aria-label="Concurrent scans" className="w-12" />
        <NumberInputIncrement />
      </NumberInputGroup>
    </NumberInputRoot>
  )
}
```

### Sizes

`size` on `NumberInput` or `NumberInputRoot` sets the height of the field and its buttons: `sm`
(28px), `default` (32px) and `lg` (36px), the heights of Input and Button. The group is as tall
as an Input of the same size, border included, and the stepper icons follow the size (14px at
`sm`, 16px otherwise).

```tsx
import { NumberInputDecrement, NumberInputField, NumberInputGroup, NumberInputIncrement, NumberInputRoot } from "@zenoxai/ui/components/number-input"

function NumberInputSizes() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      {(["sm", "default", "lg"] as const).map((size) => (
        <NumberInputRoot key={size} size={size} defaultValue={3} min={1}>
          <NumberInputGroup>
            <NumberInputDecrement />
            <NumberInputField
              aria-label={`Concurrent scans, ${size}`}
              className="w-12"
            />
            <NumberInputIncrement />
          </NumberInputGroup>
        </NumberInputRoot>
      ))}
    </div>
  )
}
```

### Formatted

`format` takes `Intl.NumberFormat` options: percent, currency, units, fraction digits. The
value stays a plain number (`0.8` for 80%); only the text in the field is formatted, in the
user's locale unless you pass `locale`.

```tsx
import { NumberInput } from "@zenoxai/ui/components/number-input"

function NumberInputFormatted() {
  return (
    <>
      <NumberInput
        defaultValue={0.8}
        min={0}
        max={1}
        step={0.05}
        format={{ style: "percent" }}
        ariaLabel="Detection confidence threshold"
      />
      <NumberInput
        defaultValue={1200}
        min={0}
        step={100}
        format={{
          style: "currency",
          currency: "USD",
          maximumFractionDigits: 0,
        }}
        ariaLabel="Monthly scan budget"
        inputClassName="w-24"
      />
    </>
  )
}
```

### Controlled

`onValueChange` receives a `number`, or `null` when the field is empty. Type the state as
`number | null` and handle the empty case.

```tsx
import * as React from "react"
import { NumberInputDecrement, NumberInputField, NumberInputGroup, NumberInputIncrement, NumberInputRoot } from "@zenoxai/ui/components/number-input"

function NumberInputControlled() {
  const [hours, setHours] = React.useState<number | null>(6)

  return (
    <div className="grid justify-items-center gap-3">
      <NumberInputRoot value={hours} onValueChange={setHours} min={1} max={24}>
        <NumberInputGroup>
          <NumberInputDecrement />
          <NumberInputField
            aria-label="Scan interval in hours"
            className="w-12"
          />
          <NumberInputIncrement />
        </NumberInputGroup>
      </NumberInputRoot>
      <p className="text-sm text-text-secondary">
        {hours === null
          ? "Enter an interval."
          : `Assets are scanned every ${hours} ${hours === 1 ? "hour" : "hours"}.`}
      </p>
    </div>
  )
}
```

## Accessibility

- The control is a text input with `aria-roledescription="Number field"`, so screen readers
  read the formatted text ("80%"). Name it with `ariaLabel`, or with `id` and a `FieldLabel`.
  A plain `aria-label` on `NumberInput` lands on the wrapper, not on the input.
- ArrowUp and ArrowDown change the value by `step`, with Shift by `largeStep` (10 by
  default) and with Alt by `smallStep` (0.1 by default). Home and End jump to `min` and `max`.
- The value is clamped to `min` and `max` when the field loses focus.

## API reference

`import { ... } from "@zenoxai/ui/components/number-input"`

### NumberInput

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| allowOutOfRange | `boolean` | `false` | When true, direct text entry may be outside the `min`/`max` range without clamping, so native range underflow/overflow validation can occur. Step-based interactions (keyboard arrows, buttons, wheel, scrub) still clamp. |
| allowWheelScrub | `boolean` | `false` | Whether to allow the user to scrub the input value with the mouse wheel while focused and hovering over the input. |
| ariaLabel | `string` |  | Accessible name of the input, when no visible label points at it. |
| defaultValue | `number` |  | The uncontrolled value of the field when it's initially rendered.  To render a controlled number field, use the `value` prop instead. |
| disabled | `boolean` | `false` | Whether the component should ignore user interaction. |
| form | `string` |  | Identifies the form that owns the hidden input. Useful when the number field is rendered outside the form. |
| format | `NumberFormatOptions` |  | Options to format the input value. |
| id | `string` |  | The id of the input element. |
| inputClassName | `string` |  | Classes for the input; `className` styles the root. |
| inputRef | `Ref<HTMLInputElement>` |  | A ref to access the hidden input element. Ref to the input element, to focus it from outside. |
| largeStep | `number` | `10` | The large step value of the input element when incrementing while the shift key is held. Snaps to multiples of this value when `snapOnStep` is enabled. |
| locale | `LocalesArgument` |  | The locale of the input element. Defaults to the user's runtime locale. |
| max | `number` |  | The maximum value of the input element. |
| min | `number` |  | The minimum value of the input element. |
| name | `string` |  | Identifies the field when a form is submitted. |
| onValueChange | `((value: number \| null, eventDetails: NumberFieldRootChangeEventDetails) => void)` |  | Callback fired when the number value changes.  The `eventDetails.reason` indicates what triggered the change: - `'input-change'` for parseable typing or programmatic text updates - `'input-clear'` when the field becomes empty - `'input-blur'` when formatting (and clamping, if enabled) occurs on blur - `'input-paste'` for paste interactions - `'keyboard'` for arrow-key/Home/End stepping (typing digits uses `'input-change'`/`'input-clear'`) - `'increment-press'` / `'decrement-press'` for button presses on the increment and decrement controls - `'wheel'` for wheel-based scrubbing - `'scrub'` for scrub area drags |
| onValueCommitted | `((value: number \| null, eventDetails: NumberFieldRootCommitEventDetails) => void)` |  | Callback function that is fired when the value is committed. It runs later than `onValueChange`, when: - The input is blurred after typing a value. - The pointer is released after scrubbing or pressing the increment/decrement buttons.  It runs simultaneously with `onValueChange` when interacting with the keyboard or the mouse wheel.  **Warning**: This is a generic event not a change event. |
| readOnly | `boolean` | `false` | Whether the user should be unable to change the field value. |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, NumberFieldRootState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |
| required | `boolean` | `false` | Whether the user must enter a value before submitting a form. |
| size | `"default" \| "sm" \| "lg"` |  | Height of the field and its steppers: `sm` 28px, `default` 32px, `lg` 36px, the heights of Input and Button. |
| smallStep | `number` | `0.1` | The small step value of the input element when incrementing while the alt key is held. Snaps to multiples of this value when `snapOnStep` is enabled. |
| snapOnStep | `boolean` | `false` | Whether the value should snap to the nearest step when incrementing or decrementing. |
| step | `number \| "any"` | `1` | Amount to increment and decrement with the buttons and arrow keys, or to scrub with pointer movement in the scrub area. To always enable step validation on form submission, specify the `min` prop explicitly in conjunction with this prop. Specify `step="any"` to always disable step validation; interactive stepping then uses a base amount of `1`, while the alt and shift keys still step by `smallStep` and `largeStep`. |
| value | `number \| null` |  | The raw numeric value of the field. |

### NumberInputRoot

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| allowOutOfRange | `boolean` | `false` | When true, direct text entry may be outside the `min`/`max` range without clamping, so native range underflow/overflow validation can occur. Step-based interactions (keyboard arrows, buttons, wheel, scrub) still clamp. |
| allowWheelScrub | `boolean` | `false` | Whether to allow the user to scrub the input value with the mouse wheel while focused and hovering over the input. |
| defaultValue | `number` |  | The uncontrolled value of the field when it's initially rendered.  To render a controlled number field, use the `value` prop instead. |
| disabled | `boolean` | `false` | Whether the component should ignore user interaction. |
| form | `string` |  | Identifies the form that owns the hidden input. Useful when the number field is rendered outside the form. |
| format | `NumberFormatOptions` |  | Options to format the input value. |
| id | `string` |  | The id of the input element. |
| inputRef | `Ref<HTMLInputElement>` |  | A ref to access the hidden input element. Ref to the input element, to focus it from outside. |
| largeStep | `number` | `10` | The large step value of the input element when incrementing while the shift key is held. Snaps to multiples of this value when `snapOnStep` is enabled. |
| locale | `LocalesArgument` |  | The locale of the input element. Defaults to the user's runtime locale. |
| max | `number` |  | The maximum value of the input element. |
| min | `number` |  | The minimum value of the input element. |
| name | `string` |  | Identifies the field when a form is submitted. |
| onValueChange | `((value: number \| null, eventDetails: NumberFieldRootChangeEventDetails) => void)` |  | Callback fired when the number value changes.  The `eventDetails.reason` indicates what triggered the change: - `'input-change'` for parseable typing or programmatic text updates - `'input-clear'` when the field becomes empty - `'input-blur'` when formatting (and clamping, if enabled) occurs on blur - `'input-paste'` for paste interactions - `'keyboard'` for arrow-key/Home/End stepping (typing digits uses `'input-change'`/`'input-clear'`) - `'increment-press'` / `'decrement-press'` for button presses on the increment and decrement controls - `'wheel'` for wheel-based scrubbing - `'scrub'` for scrub area drags |
| onValueCommitted | `((value: number \| null, eventDetails: NumberFieldRootCommitEventDetails) => void)` |  | Callback function that is fired when the value is committed. It runs later than `onValueChange`, when: - The input is blurred after typing a value. - The pointer is released after scrubbing or pressing the increment/decrement buttons.  It runs simultaneously with `onValueChange` when interacting with the keyboard or the mouse wheel.  **Warning**: This is a generic event not a change event. |
| readOnly | `boolean` | `false` | Whether the user should be unable to change the field value. |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, NumberFieldRootState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |
| required | `boolean` | `false` | Whether the user must enter a value before submitting a form. |
| size | `"default" \| "sm" \| "lg"` | `default` | Height of the field and its steppers: `sm` 28px, `default` 32px, `lg` 36px, the heights of Input and Button. |
| smallStep | `number` | `0.1` | The small step value of the input element when incrementing while the alt key is held. Snaps to multiples of this value when `snapOnStep` is enabled. |
| snapOnStep | `boolean` | `false` | Whether the value should snap to the nearest step when incrementing or decrementing. |
| step | `number \| "any"` | `1` | Amount to increment and decrement with the buttons and arrow keys, or to scrub with pointer movement in the scrub area. To always enable step validation on form submission, specify the `min` prop explicitly in conjunction with this prop. Specify `step="any"` to always disable step validation; interactive stepping then uses a base amount of `1`, while the alt and shift keys still step by `smallStep` and `largeStep`. |
| value | `number \| null` |  | The raw numeric value of the field. |

### NumberInputGroup

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, NumberFieldGroupState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

### NumberInputField

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| aria-roledescription | `string` | `'Number field'` | A user-friendly description of the input's role for assistive tech. This is a role description, not an accessible name — use `Field.Label` or `aria-label` to name the control. |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, NumberFieldInputState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

### NumberInputIncrement

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| nativeButton | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, NumberFieldIncrementState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

### NumberInputDecrement

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| nativeButton | `boolean` | `true` | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, NumberFieldDecrementState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

### NumberInputScrubArea

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| direction | `"horizontal" \| "vertical"` | `'horizontal'` | Cursor movement direction in the scrub area. |
| pixelSensitivity | `number` | `2` | Determines how many pixels the cursor must move before the value changes. A higher value will make scrubbing less sensitive. |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, NumberFieldScrubAreaState>` |  | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |
| teleportDistance | `number` |  | If specified, determines the distance that the cursor may move from the center of the scrub area before it will loop back around. |

