# NumberInput

A numeric field with stacked step buttons at its end, for quantities, counts and amounts with a known step (cart quantity, guest count, seats). It is a text input (type text, inputmode decimal), never type number, so it can show formatted text such as 1.000 ₫. Use Input for free text, Slider when the exact value matters less than the range.

## Usage

### When to use

- Enter or adjust a quantity whose valid steps are known, such as seats, guests or items in a cart.
- Show a number with locale-aware formatting, such as a currency amount, while keeping it editable.

### When not to use

- Enter an identifier such as a phone or card number. Use Input with numeric input mode.
- Pick a position where the range matters more than the exact number. Use Slider.
- Enter free-form text. Use Input.

## Variants

### size

Control height from size-element-sm/md/lg/xl. Pick per region; md is the default.

Values: `sm`, `md`, `lg`, `xl`. Default: `md`.

## Examples

### Quantity

```tsx
<Field label="Quantity">
  <NumberInput defaultValue={1} min={1} max={10} />
</Field>
```

### Controlled

```tsx
const [guests, setGuests] = React.useState(2)
<NumberInput value={guests} onValueChange={setGuests} min={1} max={16} size="lg" />
```

### Currency

```tsx
<NumberInput defaultValue={1000} step={1000} min={0} locale="vi-VN" formatOptions={{ style: "currency", currency: "VND" }} />
```

## Do and don't

### Do

- Set min and max whenever the domain has them (a quantity is at least 1).
- Use inside Field so the label and error are linked.
- Use lg or xl on touch-first screens so the buttons are easy to hit.

### Don't

- Do not use for numbers that are identifiers (card number, phone, postal code); use Input with inputmode numeric.
- Do not use for a range where position matters more than the value; use Slider.
- Do not fire network requests on every step; debounce in the app.

## Props

| Name | Type | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `value` | `number \| null` |  |  | The value when you control it. null is an empty field. |
| `defaultValue` | `number \| null` |  | `null` | The value on first render when you do not control it. |
| `onValueChange` | `(value: number \| null) => void` |  |  | Called with the committed number (after parse, clamp and step rounding) on a step key, a button press, Enter or blur, and with null when the field is cleared. Not called for text that does not parse (the field shows invalid instead) or on every keystroke. |
| `min` | `number` |  |  | Lowest value. Buttons and ArrowDown stop here. |
| `max` | `number` |  |  | Highest value. Must be greater than min. |
| `step` | `number` |  | `1` | Amount per step. |
| `largeStep` | `number` |  |  | Amount for PageUp and PageDown. Defaults to step times 10. |
| `clampOnBlur` | `boolean` |  | `true` | Clamp and round a typed value when the field loses focus. When false, an out-of-range value stays and the field shows invalid. |
| `size` | `enum` | `sm`, `md`, `lg`, `xl` | `md` | Control height from size-element-sm/md/lg/xl. Pick per region; md is the default. |
| `disabled` | `boolean` |  | `false` | No typing, no buttons, disabled look. |
| `readOnly` | `boolean` |  | `false` | The value can be read, focused and copied but not changed; the step buttons are hidden and the field drops its fill. The inner input gets the native readOnly attribute (its bindings use :read-only); the root group sets data-read-only. |
| `invalid` | `boolean` |  | `false` | Sets aria-invalid and the danger border. |
| `required` | `boolean` |  | `false` | Native required on the input. |
| `name` | `string` |  |  | Form field name; the submitted value is the plain number, not the formatted text. |
| `showStepButtons` | `boolean` |  | `true` | Show the decrement and increment buttons. Hide them for dense tables where arrow keys are enough. |
| `decrementLabel` | `string` |  | `Decrease` | Accessible name of the decrement button. Vietnamese: Giảm. |
| `incrementLabel` | `string` |  | `Increase` | Accessible name of the increment button. Vietnamese: Tăng. |
| `locale` | `string` |  |  | BCP 47 locale for formatting and for the separators used in parsing. Defaults to the document language. |
| `formatOptions` | `Intl.NumberFormatOptions` |  |  | How the value is shown when the field is not being edited and in aria-valuetext: currency, percent, unit, fraction digits. While focused the field shows the plain localized number. |

### Slots

| Name | Required | Description |
| --- | --- | --- |
| `leadingText` | no | Optional fixed text before the number, such as a currency (follows the planned Input leadingText). |
| `trailingText` | no | Optional unit after the number, such as kg. |

## Anatomy

| Part | Description |
| --- | --- |
| `root` | The group that draws the field box: border, fill, corners, focus ring through :has(). The step buttons sit inside it at the end. |
| `input` | The text input with role spinbutton, font-size-input, tabular numbers, aligned to the start (left in LTR, right in RTL), with the step buttons at the end. |
| `decrement` | The lower ghost icon button of the stack at the end (Tabler chevron down, 16px); first of the pair on touch. |
| `increment` | The upper ghost icon button of the stack at the end (Tabler chevron up, 16px); second of the pair on touch. |
| `placeholder` | The placeholder text of the input, in secondary text like Input. |

## Accessibility

Role: `spinbutton`

| Keys | Action |
| --- | --- |
| `ArrowUp / ArrowDown` | Increase or decrease by step, clamped to min and max. |
| `PageUp / PageDown` | Increase or decrease by largeStep. |
| `Home / End` | Set min or max when they are set. |
| `Enter` | Commit the typed text: parse, clamp, round to step. |
| `Tab` | Moves to the next field. The step buttons are skipped (tabIndex -1); the keys above cover them. |

- The input has type text, inputmode decimal (numeric when step is an integer and min is at least 0) and role spinbutton with aria-valuenow, aria-valuemin and aria-valuemax.
- aria-valuetext is the value formatted with formatOptions and locale, for example 1.000 ₫, so screen readers say the same text the field shows.
- Parsing: the group and decimal separators of the locale come from Intl.NumberFormat formatToParts. Group separators are stripped, the decimal separator maps to a dot, then the text is parsed. A string that does not parse sets the invalid state and does not call onValueChange.
- Clearing the field calls onValueChange(null).
- Step buttons are real buttons named by decrementLabel and incrementLabel, disabled at min and max, skipped by Tab (tabIndex -1) because the arrow keys cover them.
- Layout: on a fine pointer the two buttons stack at the end of the field, increment on top. On touch (pointer: coarse) they sit side by side at the end, decrement then increment, each at least size-target-min (44px), with a color-border divider on each button's start edge so the touch area is visible.
- Holding a step button repeats after a short delay; the announcement is the final value, not each step.
- invalid sets aria-invalid; Field passes its error with aria-describedby.

## Install

```bash
npx werewolf-ui add number-input
```

Run `npx werewolf-ui init` once in your app before adding components. Init sets up tokens, their CSS import and agent rules. Add installs this component and its dependencies while preserving your theme. See [Installation](https://werewolf-ui.vercel.app/docs/installation.md) for requirements and the alternative registry setup.
