Skip to content

NumberField

A field that only takes a number, with steppers to nudge the value, range clamping and formatted display.

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; 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
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
incrementLabelstringAccessible name of the increment button
decrementLabelstringAccessible 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
shortcutsNebaShortcuts<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
placeholderstringShown in the field while it is empty
requiredbooleanWhether a value is required before the form is submitted
classNamesNebaSlots<'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.

tsx
<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.

tsx
<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 an aria-roledescription; beside it a hidden <input type="number"> holds min, max and step and handles form submission and browser validation. Keeping the two apart is what lets the visible field show $1,240.
  • label becomes the accessible name; the steppers are named by incrementLabel and decrementLabel.
  • The steppers stay out of the tab order, because the arrow keys on the field do the same job.
  • A stepper that has reached min or max becomes disabled.
  • locale names the two steppers, so a plain BCP 47 string keeps the digits and the buttons in one language.

Released under the MIT License