NumberField
A field that only takes a number, with steppers to nudge the value, range clamping and formatted display.
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; a plain BCP 47 string also names the two steppers |
| 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 | — | Accessible name of the increment button |
| decrementLabel | string | — | 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 |
| shortcuts | NebaShortcuts<HTMLInputElement> | — | Key combinations to act on, spelled the way Shortcut draws them: { 'Mod+Enter': send }. Mod is Command on a Mac and Control everywhere else. Bound to the input rather than to the root: a plain onKeyDown lands on the column holding the label and the messages, so its currentTarget is not the field |
| placeholder | string | — | Shown in the field while it is empty |
| required | boolean | — | Whether a value is required before the form is submitted |
| classNames | NebaSlots<'label' | 'shell' | 'control' | 'description' | 'error' | 'stepper'> | — | Class names for the parts behind the root. The root itself is className, so there is no root key |
Native <div> attributes pass through to the root. Only color and defaultValue are excluded, since the table above spells them differently.
value is a number | null, where null means empty. It is never a string you have to parse.
The shell is identical to TextField's, so a field of the same size lines up beside it.
Examples
steppers
end groups the increment and decrement buttons at the right of the field. 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 leaves keyboard input.
step · largeStep · smallStep
The arrow keys move by step, Shift by largeStep and Alt by smallStep. snapOnStep rounds the result to a multiple of step.
allowWheelScrub is off by default. Turning it on lets the wheel change the value, at the cost of sharing a gesture with the page's scroll.
format and locale
format is Intl.NumberFormatOptions. The field can show $1,240 or 7.5% while value stays 1240 and 0.075.
variant
size
The steppers are sized in em, so they track the number. The field lines up with a Button, TextField or Select of the same size.
disabled · readOnly · error
readOnly removes the steppers rather than leaving them disabled. The number stays selectable so it can be copied out.
shortcuts
shortcuts maps a key combination to what it does, written the way Shortcut draws it. Mod is Command on a Mac and Control everywhere else, and the modifiers are matched exactly: Enter and Mod+Enter never both fire.
<NumberField label="Quantity" shortcuts={{ Enter: commit }} />It is bound to the <input> rather than to the root, which is what makes it worth having here: className and a plain onKeyDown both land on the column holding the label and the two lines under it, so their currentTarget is not the field.
Nothing is prevented for you. A shortcut on ArrowUp fires and the field still steps; call preventDefault in the handler if it should not.
classNames
className lands on the root (the column holding the label, the shell and the two lines under it), so the <input> is reached through classNames.control.
<NumberField label="Seats" classNames={{ control: 'text-right', stepper: 'rounded-none' }} />The slots are label, shell, control, description, error and stepper. stepper is both buttons rather than one each: a pair of steppers that do not match is not a thing anyone is building. See prop conventions for how a class name you pass resolves against the component's own.
Accessibility
- The visible control is a text input with
inputmode="numeric"and anaria-roledescription; beside it a hidden<input type="number">holdsmin,maxandstepand handles form submission and browser validation. Keeping the two apart is what lets the visible field show$1,240. labelbecomes the accessible name; the steppers are named byincrementLabelanddecrementLabel.- The steppers stay out of the tab order, because the arrow keys on the field do the same job.
- A stepper that has reached
minormaxbecomesdisabled. localenames the two steppers, so a plain BCP 47 string keeps the digits and the buttons in one language.