Number field
StableA number field with − and + steppers, following the APG spinbutton: arrows step, Page keys step ×10, Home and End jump.
A text input with inputmode="numeric" rather than type="number", which changes on a mouse wheel and accepts “e”. Typing is free; the value is clamped to min and max and snapped to the step when the field loses focus. The steppers are for pointer and touch and stay out of the tab order, as in the APG example.
Budget
import { useState } from 'react'import { NumberField } from '@rchursin/product/forms'import { Field } from '@rchursin/product/ui'
/** ↑ ↓ step by $5, Page Up / Page Down by $50, Home and End jump to $0 and $500. */export default function NumberFieldBudget() { const [budget, setBudget] = useState(40)
return ( <div className="w-full max-w-xs"> <Field label="Budget alert" hint="Per agent, per week. Sample setting."> {(p) => <NumberField {...p} value={budget} onChange={setBudget} min={0} max={500} step={5} prefix="$" suffix="a week" noun="budget" />} </Field> </div> )}Usage
import { NumberField, type NumberFieldProps } from '@rchursin/product/forms'<Field label="Budget alert" hint="Per agent, per week."> {(p) => ( <NumberField {...p} value={budget} onChange={setBudget} min={0} max={500} step={5} prefix="$" noun="budget" /> )}</Field>Examples
Plain and read-only
No prefix or unit, in the large size; and read-only, where the steppers switch off but the value stays focusable.
import { useState } from 'react'import { NumberField } from '@rchursin/product/forms'import { Field } from '@rchursin/product/ui'
/** No prefix or unit, in the large size; and read-only, where the steppers switch off but the value stays focusable. */export default function NumberFieldPlain() { const [retries, setRetries] = useState(2)
return ( <div className="flex w-full max-w-xs flex-col gap-5"> <Field label="Retries" hint="Before a run counts as failed. 0 to 5."> {(p) => <NumberField {...p} size="lg" value={retries} onChange={setRetries} min={0} max={5} noun="retries" />} </Field> <Field label="Seats" hint="Set by your plan."> {(p) => <NumberField {...p} value={12} onChange={() => {}} readOnly noun="seats" />} </Field> </div> )}API
Number field props
| Prop | Type | Default | Description |
|---|---|---|---|
valuerequired | number | – | The current number. |
onChangerequired | (value: number) => void | – | Called while typing when the text is a number inside the range, on every step, and with the clamped value on blur. |
min | number | 0 | The lowest value; Home jumps to it. |
max | number | Number.MAX_SAFE_INTEGER | The highest value. End only works when a max is set. |
step | number | 1 | One arrow press. Page Up / Page Down move ten steps, and values snap to multiples of it. |
prefix | string | – | Shown before the number, e.g. "$". Part of the spoken value. |
suffix | string | – | Shown after it, e.g. "a week". Also part of the spoken value. |
noun | string | 'value' | Names the steppers: “Decrease budget”, “Increase budget”. |
size | 'md' | 'lg' | 'md' | Height 32 or 40 pixels. |
readOnly | boolean | – | The value stays focusable and readable; keys and steppers stop. |
disabled | boolean | – | Dims the field and turns everything off. |
id | string | – | id of the input. Set by Field’s render prop ({...p}), along with aria-describedby and aria-invalid. |
className | string | – | Classes for the wrapper. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Up arrowDown arrow | Steps up or down by one step. |
| Page UpPage Down | Steps by ten steps. |
| Home | Jumps to the minimum. |
| End | Jumps to the maximum, when one is set. |
| Tab | Leaves the field and settles the typed value inside the range. The steppers are skipped. |
- The input has role="spinbutton" with aria-valuenow, aria-valuemin and aria-valuemax (left out when there is no limit), and aria-valuetext that includes the prefix and suffix: “$40 a week”.
- The steppers are buttons named from noun (“Decrease budget”), point at the input with aria-controls, and turn disabled at either end.
- Whole numbers only: typing keeps digits and a minus sign. inputmode="numeric" brings up the number pad on phones.
Guidelines
Do
- Set min and max whenever the value has real limits, so Home and End work.
- Use a prefix or suffix for the unit instead of writing it in the label.
- Choose a step people think in: 5 dollars, 15 minutes.
Don’t
- Don’t use it for numbers that aren’t amounts: card numbers, codes or years belong in a text field.
- Don’t use it for decimals; it keeps whole numbers only.
In use
Related
- Range sliderThe lux-range finish for a native range input: a hairline track that fills to the thumb and a lit pearl thumb.
- Text inputA one-line field in the inset lux-input finish, in two sizes, with an optional icon inside and a slot on the right.
- FieldLabel above, hint under it, error in words between the hint and the control, with the ids and aria wiring done for you.
Source
The component’s own files, exactly as they run on this site.
/** * Number field with − and + steppers (WAI-ARIA APG spinbutton). * * <Field label="Budget alert" hint="Per agent, per week"> * {(p) => <NumberField {...p} value={40} onChange={setBudget} min={0} max={500} step={5} prefix="$" />} * </Field> * * A text input with inputmode="numeric" (type="number" scrolls on wheel and accepts "e"): * ↑ ↓ step, Page Up / Page Down step ×10, Home / End jump to min / max. Typing is free; the * value is clamped when the field loses focus. The buttons stay out of the tab order (as in the * APG example) because the arrow keys do the same; they remain for pointer and touch. */import { useEffect, useState, type KeyboardEvent } from "react"import { cx } from "../../components/cx"import { Icon } from "../icons/Icon"
export interface NumberFieldProps { id?: string value: number onChange: (value: number) => void