NumberField
A typed numeric input — Intl-formatted (decimal/currency/percent/unit) with a stepper, spinbutton a11y, and a raw number value.
@garn/ui/number-fieldDefault
A labelled quantity field with a floor of 0.
Currency
Currency formatting via formatOptions.
Percent
Percent style — the value is the fraction, displayed as a percentage.
Bounded
min/max clamp; the stepper disables at each bound.
States
Disabled and invalid.
Affordances
clearable and loading — both render to the left of the stepper, which always stays pinned at the trailing edge.
Appearances
outline · soft · ghost — the shared field-appearance axis, inherited from Input.
Sizes
xs · sm · md · lg · xl.
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add number-field- Registry
number-field- Source
- number-field.tsx
- Deps
lucide-react- Registry deps
use-controllable-stateuse-merged-refnumber-parseinput
Import
import { NumberField } from "@garn/ui/number-field";Props
Generated from the component's TypeScript types.
appearance- Description
- No description.
- Type
"ghost" | "outline" | "soft"- Default
outline
size- Description
- No description.
- Type
"lg" | "md" | "sm" | "xl" | "xs"- Default
md
decrementLabel- Description
- Accessible name for the decrement button.
- Type
string
defaultValue- Description
- Initial value for the uncontrolled case.
- Type
number
formatOptions- Description
- `Intl.NumberFormatOptions` — decimal · currency · percent · unit.
- Type
Intl.NumberFormatOptions
hideStepper- Description
- Hide the +/− stepper buttons (keyboard stepping still works).
- Type
boolean
incrementLabel- Description
- Accessible name for the increment button.
- Type
string
largeStep- Description
- PageUp/PageDown increment. Defaults to `step * 10`.
- Type
number
locale- Description
- BCP-47 locale for formatting/parsing. Defaults to the runtime locale.
- Type
string
max- Description
- Upper bound — clamped on blur/step; increment disables at it.
- Type
number
min- Description
- Lower bound — clamped on blur/step; decrement disables at it.
- Type
number
onValueChange- Description
- Fires with the committed number (or `undefined` when cleared).
- Type
(value: number | undefined) => void
step- Description
- ArrowUp/ArrowDown increment. Defaults to 0.01 for percent, else 1.
- Type
number
value- Description
- The numeric value. Controlled via `value` + `onValueChange`.
- Type
number
Plus 312 inherited native <input> attributes.
Styling
Target these data-slots and remap these tokens to restyle without forking the component.
[data-slot="input"][data-slot="input-root"][data-slot="number-field-stepper"][data-slot="number-field-increment"][data-slot="number-field-decrement"]| Property | Token | Tier |
|---|---|---|
height | --garn-control-h-md | semantic · air |
paddingInline | --garn-control-px-md | semantic · air |
Overview
NumberField is the typed numeric control — the number-side sibling of TimeField. It reuses Input wholesale (the wrapped focus surface, appearance × size, leading slot, and the invalid treatment), adding a trailing +/− stepper and the number logic. Under the hood it is a type="text" input with role="spinbutton" — never native type="number" — so it can format the value with Intl.NumberFormat (formatOptions: decimal, currency, percent, unit) while keeping the raw string editable while focused. The value is a real number: min/max clamp on blur, an explicit step snaps to the grid on blur, and stepping (↑/↓, PageUp/Down, Home/End, or the buttons) is float-safe and clamped.
Reach for a plain Input when the value is a code or identifier rather than a quantity, and a Slider when a coarse value is best chosen by dragging within a fixed range.
Guidelines
Pair with a Label and pass formatOptions that matches the quantity (currency, percent, a unit) — it drives both display and parsing. Set min/max to clamp and to disable the stepper at each bound, and an explicit step when the value should snap to a grid (percent defaults to 0.01). Read the value as a number (percent as its fraction); on form submit a hidden input carries the raw number. Let the field surface own sizing — pick a size rung rather than hand-padding.
Best practices
- Pair with a label and pass
formatOptionsfor currency/percent/unit. - Set
min/max(andstepwhen snapping) to constrain the value. - Hold the value in state (or
defaultValue) and read it as anumber.
- Don't reach for native <input type="number"> — it can't format and has unstyleable steppers.
- Don't hand-set height/padding — the size variant owns the grid.
- Don't rely on the placeholder as the label.
Content guidelines
- Label with the quantity ("Price", "Guests").
- Match
formatOptionsto the unit users expect (a currency, a percent, a unit).
Accessibility
- Role
spinbutton- Focus
- Inherits Input's focus surface (border darkens on focus; keyboard focus adds a neutral ring).
Enforceable contracts
Give every number field an accessible label.
when no associated <label htmlFor>, aria-label, or aria-labelledby
Keyboard
| ArrowUpArrowDown | Step the value by `step`. |
| PageUpPageDown | Step the value by `largeStep` (default `step` × 10). |
| HomeEnd | Jump to `min` / `max` (when set). |
- • The field is role=spinbutton with aria-valuenow / aria-valuemin / aria-valuemax and an aria-valuetext carrying the formatted display.
- • Give the field an accessible label — <label htmlFor>, aria-label, or aria-labelledby.
- • The stepper buttons are out of the tab order (keyboard users step with the arrows); they stay labelled for pointer + screen-reader users.
- • Reflect validation with aria-invalid + a described error message.
- Status
- Stable
- Version
v0.3.0- Introduced
v0.2.0
garn doesn't yet record a per-release version history. This reflects the component's current lifecycle state; full changelog entries will appear once the metadata pipeline emits a lifecycle.history field.