Number field
NumberField and MoneyField format numbers and currency for the locale, clamp to min and max, and report real numbers, never formatted strings.
View as Markdown for coding assistants, or see llms.txt.
- New
- Group: Inputs
- Added in @azmr/ui 0.8.0
Usage
import { MoneyField, NumberField, numberOrNull } from "@azmr/ui/number-field";Up to 12 per booking.
Square metres, to one decimal place.
<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 (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,maxandstepwork with the arrow keys and, withstepper, 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.decimalScalesets the most digits after the point; the number is rounded to it.grouping={false}turns off thousands separators, for codes and years.formattakes any furtherIntl.NumberFormatoptions, such as{ style: "percent" }or{ style: "unit", unit: "kilogram" }.- Pasted text is parsed for the locale, so pasting
$2,499.99gives2499.99. - The mouse wheel doesn't change a focused field unless you set
allowWheelScrub, so scrolling the page past it is safe.
Money
Excluding GST.
- Subtotal
- $712.50
- GST (15%)
- $106.88
- Total
- $819.37
<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, 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: 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:
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
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 usesdecimalwhenminis 0 or more and the full keyboard when negatives are allowed. It is labelled and described by itsField. - 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. |
| ShiftorArrowUp | Increases the value by 10. |
| ShiftorArrowDown | Decreases the value by 10. |
| AltorArrowUp | 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. |