Skip to content

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.

tsx
import { NumberField } from 'neba';

<NumberField label="Seats" defaultValue={3} min={1} max={20} />;

Props

PropTypeDefaultDescription
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
elevationshared0 | 1 | 2 | 30Drop shadow depth. 0 means no shadow at all
valuenumber | nullThe number. null means empty — never a string you have to parse
defaultValuenumberThe initial number, for an uncontrolled field
onValueChange(value: number | null) => voidCalled on every change — typing, stepping, the wheel
onValueCommitted(value: number | null) => voidCalled when the value settles: on blur after typing, on pointer release after a press
minnumberThe bottom of the range
maxnumberThe top of the range
stepnumber | 'any'1How far one step goes. 'any' turns step validation off
largeStepnumber10The step taken while Shift is held
smallStepnumber0.1The step taken while Alt is held
snapOnStepbooleanfalseWhether stepping snaps to multiples of the step
allowWheelScrubbooleanfalseWhether 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
formatIntl.NumberFormatOptionsHow the number is written — currency, percent, decimal places. The field shows $1,240 and still reports 1240
localeIntl.LocalesArgumentWhich 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
labelReactNodeThe label, wired to the control by Base UI's Field
descriptionReactNodeHelper text
errorReactNodeError message. Its presence turns the control invalid and re-points the colour family at danger
invalidboolean!!errorForces the invalid state without a message, for when a form library owns validity
startIconReactNodeContent before the number — a currency mark, a unit
endIconReactNodeContent after the number, before the steppers
fullWidthbooleanfalseStretches to the width of the container
incrementLabelstring'Increase'Accessible name of the increment button
decrementLabelstring'Decrease'Accessible name of the decrement button
namestringIdentifies the field when a form is submitted
readOnlybooleanfalseShown but not changeable. Keeps its colour and edge, drains the saturation
disabledbooleanfalseUnavailable. 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:

  • value is a number | null, and null means empty. It is never a string you have to parse.
  • format is Intl.NumberFormatOptions, so the field shows $1,240 or 7.5% and still reports 1240 and 0.075.
  • The arrow keys step by step, Shift by largeStep and Alt by smallStep.
  • The wheel does nothing unless allowWheelScrub says 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 an aria-roledescription of Number field, not <input type="number">. Beside it sits a hidden <input type="number"> holding min, max and step — that is the one a form submits and the browser validates, and keeping the two apart is what lets the visible field show $1,240 without the browser refusing to parse it.
  • label becomes the accessible name; the steppers are named by incrementLabel and decrementLabel and stay out of the tab order, because the arrow keys on the field itself already do their job.
  • A stepper that has run into min or max is disabled — it changes colour family, the way every other inert control in the library does, rather than fading.

Released under the MIT License