NumberField
A field that only holds a number. The shell is a TextField's, to the pixel; what is added is stepping, clamping and formatting.
import { NumberField } from 'neba';
<NumberField label="Seats" defaultValue={3} min={1} max={20} />;Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | 'solid' | 'outline' | 'text' | 'outline' | Weight of the surface. A TextField's shell, to the pixel |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Height and type scale. The steppers are sized in em, so they track the number |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. The surface is white, so it reaches the edge, the ring, the caret and the steppers on hover |
| densityshared | 'default' | 'compact' | 'default' | Padding only — never the height, never the type scale |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth. 0 means no shadow at all |
| value | number | null | — | The number. null means empty — never a string you have to parse |
| defaultValue | number | — | The initial number, for an uncontrolled field |
| onValueChange | (value: number | null) => void | — | Called on every change — typing, stepping, the wheel |
| onValueCommitted | (value: number | null) => void | — | Called when the value settles: on blur after typing, on pointer release after a press |
| min | number | — | The bottom of the range |
| max | number | — | The top of the range |
| step | number | 'any' | 1 | How far one step goes. 'any' turns step validation off |
| largeStep | number | 10 | The step taken while Shift is held |
| smallStep | number | 0.1 | The step taken while Alt is held |
| snapOnStep | boolean | false | Whether stepping snaps to multiples of the step |
| allowWheelScrub | boolean | false | Whether the wheel changes the value while focused and hovered. A page that scrolls under the pointer and a field that changes under it are the same gesture, and only one was meant |
| format | Intl.NumberFormatOptions | — | How the number is written — currency, percent, decimal places. The field shows $1,240 and still reports 1240 |
| locale | Intl.LocalesArgument | — | Which locale the number is written and parsed in. Defaults to the runtime's |
| steppers | 'end' | 'split' | 'none' | 'end' | Where the steppers sit. split puts them on either side of the number; none drops them |
| label | ReactNode | — | The label, wired to the control by Base UI's Field |
| description | ReactNode | — | Helper text |
| error | ReactNode | — | Error message. Its presence turns the control invalid and re-points the colour family at danger |
| invalid | boolean | !!error | Forces the invalid state without a message, for when a form library owns validity |
| startIcon | ReactNode | — | Content before the number — a currency mark, a unit |
| endIcon | ReactNode | — | Content after the number, before the steppers |
| fullWidth | boolean | false | Stretches to the width of the container |
| incrementLabel | string | 'Increase' | Accessible name of the increment button |
| decrementLabel | string | 'Decrease' | Accessible name of the decrement button |
| name | string | — | Identifies the field when a form is submitted |
| readOnly | boolean | false | Shown but not changeable. Keeps its colour and edge, drains the saturation |
| disabled | boolean | false | Unavailable. Drops the colour family for neutral grey |
Not <input type="number">
The native number input rounds the corners off in different places in every browser, ignores the locale, offers a wheel gesture that fights the page's scroll, and hands you a string that is empty when the field holds nonsense. This one keeps the pieces worth keeping and answers each of those:
valueis anumber | null, andnullmeans empty. It is never a string you have to parse.formatisIntl.NumberFormatOptions, so the field shows$1,240or7.5%and still reports1240and0.075.- The arrow keys step by
step, Shift bylargeStepand Alt bysmallStep. - The wheel does nothing unless
allowWheelScrubsays otherwise. A page that scrolls under the pointer and a field that changes under it are the same gesture, and only one of them was meant.
Examples
Steppers
end is the spinner everyone has seen. split puts the minus and the plus on either side of the number, for a quantity that is nudged rather than typed. none drops the buttons and keeps everything else.
There is deliberately no stacked pair of half-height chevrons. At xs each arrow would be under three pixels tall, and a target that small is a target nobody hits.
Formatting
value stays a plain number whatever the field is showing.
Variants
Sizes
The steppers are sized in em, so they track the number rather than carrying a ladder of their own — and the field lines up with a Button, a TextField and a Select of the same size.
States
Read-only takes the steppers away rather than disabling them: a button that is visible and refuses every press is worse than no button. The number stays selectable, because a read-only field is still something you copy out of.
Accessibility
- Base UI owns the parsing, the clamping, and the press-and-hold repeat on the steppers.
- What you see is a text input carrying
inputmode="numeric"and anaria-roledescriptionof Number field, not<input type="number">. Beside it sits a hidden<input type="number">holdingmin,maxandstep— that is the one a form submits and the browser validates, and keeping the two apart is what lets the visible field show$1,240without the browser refusing to parse it. labelbecomes the accessible name; the steppers are named byincrementLabelanddecrementLabeland stay out of the tab order, because the arrow keys on the field itself already do their job.- A stepper that has run into
minormaxisdisabled— it changes colour family, the way every other inert control in the library does, rather than fading.