NumberField Components
Headless compound components — the fastest way to build a number field.
The NumberField namespace exports headless compound components that handle all
wiring internally via React Context. No prop drilling, no ref management.
Import
import { NumberField } from "raqam";Component tree
<NumberField.Root ...> {/* context provider + state */}
<NumberField.Label /> {/* <label> */}
<NumberField.Group> {/* <div role="group"> */}
<NumberField.Decrement /> {/* <button> */}
<NumberField.Input /> {/* <input type="text"> */}
<NumberField.Increment /> {/* <button> */}
</NumberField.Group>
<NumberField.ScrubArea> {/* drag-to-change wrapper */}
<NumberField.ScrubAreaCursor /> {/* custom pointer cursor */}
</NumberField.ScrubArea>
<NumberField.Formatted /> {/* read-only formatted display */}
<NumberField.Description /> {/* helper text */}
<NumberField.ErrorMessage /> {/* validation error */}
<NumberField.HiddenInput /> {/* form submission value */}
</NumberField.Root>Only Root and Input are required. All others are optional.
NumberField.Root
Context provider. Accepts every useNumberFieldState
and useNumberField option (step, formatOptions,
validate, copyBehavior, stepHoldDelay, label, aria-*, …) — the most
common ones plus the Root-only props are:
| Prop | Type | Description |
|---|---|---|
locale | string | BCP 47 locale. Defaults to the current runtime locale. |
formatOptions | Intl.NumberFormatOptions | Number format configuration. |
value / defaultValue | number | null | Controlled/uncontrolled value. |
onChange | (v: number | null) => void | Called whenever the parsed numeric value changes. |
onValueChange | (v, details) => void | Like onChange, plus { reason, formattedValue }. |
onValueCommitted | (v, details) => void | Fires only on commit, with { reason: "blur" | "keyboard" }. |
minValue / maxValue | number | Constraints. |
step / largeStep / smallStep | number | Step sizes. |
validate | (v: number | null) => boolean | string | null | Custom validator. |
disabled / readOnly | boolean | Interaction state. |
required | boolean | Marks the field as required. |
allowNegative / allowDecimal | boolean | Input constraints. |
prefix / suffix | string | Affixes. |
name | string | Generates hidden input props for native form submission. |
allowMouseWheel | boolean | Enable wheel-based increment/decrement. |
copyBehavior | "formatted" | "raw" | "number" | Clipboard behaviour. |
allowOutOfRange | boolean | Skip clamping; show aria-invalid instead. |
className / style | string / CSSProperties | Applied to the root wrapper <div>. |
Data attributes set on the root element:
| Attribute | Set when |
|---|---|
data-focused | Input is focused |
data-invalid | Value is invalid |
data-disabled | disabled is true |
data-readonly | readOnly is true |
data-required | required is true |
data-scrubbing | A scrub interaction is active |
onValueChange fires on every change and reports how it happened via
reason ("input", "clear", "paste", "keyboard", "increment",
"decrement", "wheel", "scrub", or "blur"). onValueCommitted fires
only when the value settles — on blur (reason: "blur") or when the user
presses Enter (reason: "keyboard") — after formatting and clamping, and
receives the final value. Use it to persist a value or fire a request without
reacting to every keystroke.
<NumberField.Root
locale="en-US"
minValue={0}
onValueCommitted={(value, { reason }) => save(value)} // reason: "blur" | "keyboard"
>
<NumberField.Input />
</NumberField.Root>See Formatting & Behavior → Change reasons for the full breakdown.
NumberField.Input
The text input. Accepts all standard <input> props except type (always "text").
| Prop | Type | Description |
|---|---|---|
render | RenderProp | Swap the element (e.g. a styled component). |
NumberField.Label
<NumberField.Label>Price</NumberField.Label>Renders a <label> with htmlFor wired to the input id. Accepts all
<label> props.
NumberField.Group
Wraps the input and stepper buttons. Renders <div role="group">. Accepts all
<div> props.
NumberField.Increment / NumberField.Decrement
Stepper buttons. Accepts all <button> props.
<NumberField.Decrement>−</NumberField.Decrement>
<NumberField.Increment>+</NumberField.Increment>Buttons are automatically disabled when at minValue/maxValue or when the
field is disabled or readOnly.
Press-and-hold acceleration: hold for stepHoldDelay ms → interval fires at
stepHoldInterval ms.
Each button carries an accessible aria-label (defaults "Increase" /
"Decrease"). Localize them via the incrementLabel / decrementLabel props on
<NumberField.Root>, or override per button with aria-label on
<NumberField.Increment> / <NumberField.Decrement>.
<NumberField.Root incrementLabel="Augmenter" decrementLabel="Diminuer">
<NumberField.Decrement>−</NumberField.Decrement>
<NumberField.Increment>+</NumberField.Increment>
</NumberField.Root>NumberField.ScrubArea
Wraps any element; dragging over it adjusts the value via Pointer Lock API.
| Prop | Type | Default | Description |
|---|---|---|---|
direction | "horizontal" | "vertical" | "both" | "horizontal" | Drag axis. |
pixelSensitivity | number | 4 | Pixels dragged per step. |
label | string | "Scrub to change value" | Accessible label (aria-label) for the scrub area. |
render | RenderProp | — | Custom element. |
<NumberField.ScrubArea direction="horizontal" pixelSensitivity={2}>
Drag to adjust
</NumberField.ScrubArea>The element renders as role="slider" with tabIndex={0}, so it is keyboard
accessible: arrow keys (←/→ or ↑/↓) step the value while it is focused. It also
exposes the slider value ARIA — aria-valuenow, aria-valuemin,
aria-valuemax, aria-valuetext (the formatted value), and aria-disabled — so
assistive tech announces the current value and range. The CSS cursor reflects
the axis (ew-resize / ns-resize / move), and data-scrubbing="" is set
while a pointer-lock drag is active.
pixelSensitivity values below 1 are clamped to 1.
NumberField.ScrubAreaCursor
Optional. Renders only while scrubbing so you can show custom visual feedback
next to the active scrub handle. Accepts <span> props.
<NumberField.ScrubArea>
<span>Drag me</span>
<NumberField.ScrubAreaCursor>
↔
</NumberField.ScrubAreaCursor>
</NumberField.ScrubArea>NumberField.Formatted
A <span> (or custom element) that displays the current formatted value as
read-only text. Useful for dual-display UIs (edit + display side by side). It
renders with aria-hidden="true" so screen readers announce the value once
(from the input), not twice.
<NumberField.Formatted />
{/* Renders: "$1,234.56" */}This mirrors state.inputValue and updates live as the user edits. For
formatting outside a NumberField.Root, use
useNumberFieldFormat instead.
NumberField.Description
Renders helper text as a <p>. It is automatically associated with the input
for screen readers: while a <NumberField.Description> is mounted, the input's
aria-describedby points at it. The reference is wired up after mount (the same
way aria-labelledby is) and removed when the description unmounts, so there is
never a dangling reference. See
Accessibility.
<NumberField.Root locale="en-US">
<NumberField.Input />
<NumberField.Description>
Enter a value between 0 and 100.
</NumberField.Description>
</NumberField.Root>If you also pass aria-describedby on <NumberField.Input>, your value is
merged with the description id (your value first), not dropped — e.g.
aria-describedby="my-hint amount-description".
NumberField.ErrorMessage
Displays validation errors. Has role="alert" and is only shown when the field
is invalid.
If you pass no children, it renders state.validationError automatically:
<NumberField.ErrorMessage />
{/* Renders: "Must be a positive number" */}Or supply your own content:
<NumberField.ErrorMessage>
Custom error text.
</NumberField.ErrorMessage>NumberField.HiddenInput
An <input type="hidden"> for HTML form submission. Its value is always the
raw number (no formatting). To enable it, pass name to NumberField.Root.
<NumberField.Root locale="en-US" name="price">
<NumberField.Input />
<NumberField.HiddenInput />
</NumberField.Root>render prop
The visual building blocks (Label, Group, Input, Increment,
Decrement, ScrubArea, ScrubAreaCursor, and Formatted) accept a
render prop for element replacement (no asChild peer deps required):
<NumberField.Input
render={(props) => <MyStyledInput {...props} />}
/>Use data-focused, data-invalid, data-disabled on NumberField.Root
for pure-CSS state styling — no JavaScript class toggling needed.