# Number field

> NumberField and MoneyField format numbers and currency for the locale, clamp to min and max, and report real numbers, never formatted strings.

Source: https://platform.azmara.io/docs/components/number-field

Group: Inputs · Status: new · Added in @azmr/ui 0.8.0

## Usage

```tsx
import { MoneyField, NumberField, numberOrNull } from "@azmr/ui/number-field";
```

```tsx
import { Field } from "@azmr/ui/field";
import { NumberField } from "@azmr/ui/number-field";

export default function NumberFieldBasic() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-4">
      <Field label="Guests" description="Up to 12 per booking.">
        <NumberField name="guests" stepper min={1} max={12} defaultValue={2} />
      </Field>
      <Field label="Floor area" description="Square metres, to one decimal place.">
        <NumberField name="area" min={0} decimalScale={1} defaultValue={1250.5} />
      </Field>
      <Field label="Discount">
        <NumberField
          name="discount"
          min={0}
          max={1}
          step={0.05}
          format={{ style: "percent" }}
          defaultValue={0.1}
        />
      </Field>
    </div>
  );
}
```

`NumberField` formats with `Intl.NumberFormat` for the locale from [`LocaleProvider`](https://platform.azmara.io/docs/guide/theming.md) (en-NZ without one): grouping separators, a fixed number of decimals, percentages, units. `onValueChange` gets a `number`, or `null` when the field is emptied, never the formatted text.

- `min`, `max` and `step` work with the arrow keys and, with `stepper`, the − and + buttons. Hold Shift for a step of 10, or Alt for a step of 0.1.
- With `clamp` (on by default), an out-of-range number is moved to the nearest limit as it is typed. The text catches up when the field loses focus.
- `allowNegative={false}` blocks the minus key and clamps at 0.
- `decimalScale` sets the most digits after the point; the number is rounded to it.
- `grouping={false}` turns off thousands separators, for codes and years.
- `format` takes any further `Intl.NumberFormat` options, such as `{ style: "percent" }` or `{ style: "unit", unit: "kilogram" }`.
- Pasted text is parsed for the locale, so pasting `$2,499.99` gives `2499.99`.
- The mouse wheel doesn't change a focused field unless you set `allowWheelScrub`, so scrolling the page past it is safe.

## Money

```tsx
"use client";

import { Field } from "@azmr/ui/field";
import { InputGroup, InputGroupAddon } from "@azmr/ui/input-group";
import { MoneyField, NumberField } from "@azmr/ui/number-field";
import { useState } from "react";

const GST = 0.15;
const nzd = new Intl.NumberFormat("en-NZ", { style: "currency", currency: "NZD" });

export default function NumberFieldMoney() {
  const [rate, setRate] = useState<number | null>(95);
  const [hours, setHours] = useState<number | null>(7.5);
  const subtotal = (rate ?? 0) * (hours ?? 0);

  return (
    <div className="flex w-full max-w-sm flex-col gap-4">
      <Field label="Hourly rate" description="Excluding GST.">
        <MoneyField name="rate" min={0} value={rate} onValueChange={setRate} />
      </Field>
      <Field label="Hours">
        <InputGroup>
          <NumberField
            name="hours"
            min={0}
            step={0.25}
            decimalScale={2}
            value={hours}
            onValueChange={setHours}
          />
          <InputGroupAddon>hours</InputGroupAddon>
        </InputGroup>
      </Field>
      <dl className="grid grid-cols-[1fr_auto] gap-x-4 gap-y-1 text-sm tabular-nums">
        <dt className="text-muted-foreground">Subtotal</dt>
        <dd>{nzd.format(subtotal)}</dd>
        <dt className="text-muted-foreground">GST (15%)</dt>
        <dd>{nzd.format(subtotal * GST)}</dd>
        <dt className="font-medium">Total</dt>
        <dd className="font-medium">{nzd.format(subtotal * (1 + GST))}</dd>
      </dl>
    </div>
  );
}
```

`MoneyField` is a `NumberField` formatted as currency: `$1,234.50` for NZD in en-NZ. The currency comes from `LocaleProvider` (NZD without one) or the `currency` prop. Amounts are rounded to the currency's minor unit (2 decimals for NZD, none for JPY) unless `decimalScale` says otherwise. `currencyDisplay="code"` shows `NZD 1,234.50`. The value is a plain number in dollars, not cents.

Do money arithmetic on the server or in integer cents; the demo above multiplies floats only to show a running total.

`NumberField` and `MoneyField` also fit inside an [Input group](https://platform.azmara.io/docs/components/input-group.md), for a unit or a currency code as an addon. The addon text joins the field's description, as it does for `InputGroupInput`.

## Debounce and commit

They follow the same contract as [Input](https://platform.azmara.io/docs/components/input.md#debounce-and-commit): `debounceMs` makes `onValueChange` wait, `onCommit` fires when the person is done (on blur, on Enter, or after a step with the keyboard, buttons or wheel), `data-pending` marks a waiting value, and the same number is never reported twice.

## With react-hook-form

`register()` works without a `Controller`. Base UI keeps the raw number in a hidden `<input type="number">` that carries the `name`; `ref` points at it, and focusing it focuses the visible field, so React Hook Form's focus-on-error lands in the right place. The `onChange` and `onBlur` from `register()` get real DOM events on that input once the new number is in it. Add `setValueAs: numberOrNull` to get a `number | null` rather than text:

```tsx
import { useForm } from "react-hook-form";
import { Field } from "@azmr/ui/field";
import { MoneyField, NumberField, numberOrNull } from "@azmr/ui/number-field";

type Line = { quantity: number | null; price: number | null };

export function LineItem({ onSave }: { onSave: (line: Line) => void }) {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<Line>({ defaultValues: { quantity: 1, price: null } });

  return (
    <form noValidate onSubmit={handleSubmit(onSave)}>
      <Field label="Quantity">
        <NumberField stepper min={1} {...register("quantity", { setValueAs: numberOrNull })} />
      </Field>
      <Field label="Unit price" error={errors.price?.message}>
        <MoneyField
          {...register("price", {
            setValueAs: numberOrNull,
            validate: (value) => (value === null ? "Enter a price" : true),
          })}
        />
      </Field>
    </form>
  );
}
```

`defaultValues` show up in the field. After `reset()` with new values, remount the field (for example with a `key`) so it reads them.

## Props

| Prop                | Type                                                                         | Default                                            | Required | Description                                                                                                                                                                  |
| ------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `aria-describedby`  | `string`                                                                     |                                                    | No       | Identifies the element (or elements) that describes the object.                                                                                                              |
| `aria-errormessage` | `string`                                                                     |                                                    | No       | Identifies the element that provides an error message for the object.                                                                                                        |
| `aria-invalid`      | `false \| true \| "false" \| "true" \| "grammar" \| "spelling" \| undefined` |                                                    | No       | Indicates the entered value does not conform to the format expected by the application.                                                                                      |
| `aria-label`        | `string`                                                                     |                                                    | No       | Defines a string value that labels the current element.                                                                                                                      |
| `aria-labelledby`   | `string`                                                                     |                                                    | No       | Identifies the element (or elements) that labels the current element.                                                                                                        |
| `aria-required`     | `false \| true \| "false" \| "true" \| undefined`                            |                                                    | No       | Indicates that user input is required on the element before a form may be submitted.                                                                                         |
| `value`             | `number \| null \| undefined`                                                |                                                    | No       | The number, or `null` when empty. Pass it with `onValueChange` for a controlled field.                                                                                       |
| `defaultValue`      | `number \| null \| undefined`                                                |                                                    | No       |                                                                                                                                                                              |
| `onValueChange`     | `((value: number \| null) => void) \| undefined`                             |                                                    | No       | The new number, or `null` when the field is emptied. Waits `debounceMs` when set.                                                                                            |
| `onCommit`          | `((value: number \| null) => void) \| undefined`                             |                                                    | No       | Called when the person is done with the value: on blur, on Enter, or after a step with the keyboard, the buttons or the wheel. Never twice for the same value.               |
| `debounceMs`        | `number`                                                                     |                                                    | No       | Waits this long after the last keystroke before calling `onValueChange`.                                                                                                     |
| `min`               | `string \| number \| undefined`                                              |                                                    | No       | A string is accepted too, so `{...register()}` type-checks; it is read as a number.                                                                                          |
| `max`               | `string \| number \| undefined`                                              |                                                    | No       |                                                                                                                                                                              |
| `step`              | `number`                                                                     | `1`                                                | No       | How much the arrow keys and the buttons change the value.                                                                                                                    |
| `clamp`             | `boolean \| undefined`                                                       | `true`                                             | No       | Keeps the value between `min` and `max`: an out-of-range value is moved to the nearest limit when the field is committed.                                                    |
| `allowNegative`     | `boolean \| undefined`                                                       | `true`                                             | No       | `false` stops negative numbers: the minus key does nothing and the value is clamped at 0.                                                                                    |
| `decimalScale`      | `number`                                                                     |                                                    | No       | The most digits after the decimal point. Values are rounded to it when committed.                                                                                            |
| `grouping`          | `boolean \| undefined`                                                       | `true`                                             | No       | Thousands separators, such as `1,234,567`.                                                                                                                                   |
| `locale`            | `string`                                                                     |                                                    | No       | Overrides the locale from `LocaleProvider` (en-NZ without one).                                                                                                              |
| `format`            | `Intl.NumberFormatOptions \| undefined`                                      |                                                    | No       | Further `Intl.NumberFormat` options, such as `{ style: "percent" }` or a `unit`.                                                                                             |
| `stepper`           | `boolean \| undefined`                                                       |                                                    | No       | Shows − and + buttons either side of the input.                                                                                                                              |
| `allowWheelScrub`   | `boolean \| undefined`                                                       | `false`                                            | No       | Lets the mouse wheel change the value while the field has focus. Off by default, so scrolling the page past a focused field never changes it.                                |
| `name`              | `string`                                                                     |                                                    | No       |                                                                                                                                                                              |
| `id`                | `string`                                                                     |                                                    | No       |                                                                                                                                                                              |
| `placeholder`       | `string`                                                                     |                                                    | No       |                                                                                                                                                                              |
| `disabled`          | `boolean \| undefined`                                                       |                                                    | No       |                                                                                                                                                                              |
| `readOnly`          | `boolean \| undefined`                                                       |                                                    | No       |                                                                                                                                                                              |
| `required`          | `boolean \| undefined`                                                       |                                                    | No       |                                                                                                                                                                              |
| `className`         | `string`                                                                     |                                                    | No       | Styles the root element.                                                                                                                                                     |
| `onChange`          | `FormEventHandler`                                                           |                                                    | No       | For form libraries: called with a real `change` event on the hidden form input once the new number is in it, so React Hook Form's `register()` works without a `Controller`. |
| `onBlur`            | `FormEventHandler`                                                           |                                                    | No       | For form libraries: called with a `blur` event on the hidden form input.                                                                                                     |
| `stepperLabels`     | `{ decrement: string; increment: string; } \| undefined`                     | `{ decrement: "Decrease", increment: "Increase" }` | No       | Labels for the stepper buttons.                                                                                                                                              |
| `groupClassName`    | `string`                                                                     |                                                    | No       | Styles the group: the border, height, radius and background around the input and steppers. Applied last, so it wins over the defaults.                                       |
| `children`          | `ReactNode`                                                                  |                                                    | No       | Content before the input inside the border, such as a unit. Prefer `InputGroup` for more.                                                                                    |

`MoneyField` takes the same props, plus `currency` and `currencyDisplay`.

## Accessibility

- The visible input is a text field with `inputmode="numeric"`, so phones show a number keypad. On iOS, whose number pad has no decimal point or minus key, it uses `decimal` when `min` is 0 or more and the full keyboard when negatives are allowed. It is labelled and described by its `Field`.
- The stepper buttons are named "Decrease" and "Increase" (change them with `stepperLabels`) and stay out of the way of typing.
- The raw number lives in a hidden input that screen readers skip; only the formatted field is announced.
- Values are shown with the locale's own separators and currency symbol.

| Key                    | Action                               |
| ---------------------- | ------------------------------------ |
| `ArrowUp`              | Increases the value by step.         |
| `ArrowDown`            | Decreases the value by step.         |
| `Shift` or `ArrowUp`   | Increases the value by 10.           |
| `Shift` or `ArrowDown` | Decreases the value by 10.           |
| `Alt` or `ArrowUp`     | Increases the value by 0.1.          |
| `Home`                 | Sets the minimum, when there is one. |
| `End`                  | Sets the maximum, when there is one. |
| `Enter`                | Commits the value.                   |
