opsinjs
ComponentsActions and forms

NumberField

An input for a non-clinical number with increment and decrement steppers, built on Base UI NumberField.

Preview

Theme
Density
Text
Status
number-field · base base · style base-lyraOpen under the product theme

Installation

pnpm dlx shadcn@latest add @opsinjs/number-field

The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.

Usage

import { NumberField } from "@/components/ui/number-field"
<NumberField
  label="Number of copies"
  value={copies}
  onValueChange={setCopies}
  min={1}
  max={10}
  step={1}
/>

When to use it

Use it when

  • A plain quantity or count with no clinical meaning, such as the number of copies to print.
  • A value the reader nudges one at a time as readily as types it.
  • A bounded count where a floor and a ceiling the product owns keep the reader inside a range.

Do not use it when

  • It is a clinical measurement, which carries a unit, needs an out-of-range warning and is typed in whole.Use reading-input instead.
  • The number is typed once and never nudged, such as a year or a reference code.Use field instead.

Anatomy

  1. NumberFieldThe root, Base UI's NumberField.Root. Owns value, bounds, step and the keyboard contract. On neither colour axis.Controlled by value
  2. number-field-groupThe hairline frame, Base UI's NumberField.Group, holding the three controls as one object.
  3. number-field-decrementA real button named Decrease with a Minus glyph. Floors its target at 44px and disables itself at min.
  4. number-field-inputThe spinbutton input. label is applied here as aria-label. Centred, tabular-nums, body type step.Controlled by label
  5. number-field-incrementA real button named Increase with a Plus glyph. Floors its target at 44px and disables itself at max.
  • NumberField 1the root
    • number-field-group 1the hairline frame
      • number-field-decrement 1disables at min
      • number-field-input 1the spinbutton
      • number-field-increment 1disables at max

Examples

A quantity

A reader sets how many copies to print, stepping with the buttons or typing. label names the count, which is fictional and carries no unit.

Theme
Density
Text
Status
number-field-a-quantity · base base · style base-lyraOpen under the product theme

With bounds

min and max fix the range and step sets how far one press moves. The decrement button disables at the floor and the increment button at the ceiling.

Theme
Density
Text
Status
number-field-with-bounds · base base · style base-lyraOpen under the product theme

Content guidelines

Name the thing being counted in label: Number of copies, never a bare Number or a unit. Set step to the smallest change the count is measured in, 1 for a whole count. Set min and max only where a real floor and ceiling exist. A count with no upper limit takes no max.

Do

label="Number of copies", step={1}, min={1}. Names the count, steps by whole copies, floors at a real bound.

Don’t

label="Systolic" with a min and max around a reading. A measurement dressed as a count. It is a ReadingInput.

Accessibility

Audited against WCAG 2.2 AA, source and rendered. The audit is author-run, not independent, and clinical review is pending. The rendered pass was clean: no axe violations, focus visible, a 44px target floor, no loss at 320px reflow. The source pass confirmed icons are aria-hidden, neither colour axis nor a colour-only state, role tokens with no px type, and Base UI’s own keyboard and names.

What the audit fixed. The input could be named only through aria-label. The wrapper now also forwards id and aria-labelledby, suppressing the redundant aria-label when a visible label is wired so it wins the name and Label in Name holds. label stays the required default, so the change is additive.

Residual. The default still names the input through aria-label; a persistent visible label is a recommended enhancement, deferred to one central repair across the input wrappers. Base UI hard-codes autoComplete="off", acceptable for a count with no WCAG token, and surfaces no error text, since it clamps to bounds and owns validation. Every contrast pair, including the disabled ink at a bound, is unmeasured, so the report below stands in.

KeyActionNotes
TabMoves focus into the field, onto the inputThe buttons are reachable with Tab too.
Arrow Up, Arrow DownSteps the value up or down by stepStops at max going up, min going down.
Page Up, Page DownSteps by a larger amountBase UI's large step, for crossing a bounded range quickly.
Home, EndJumps to min or maxOnly where that bound is set.
EnterCommits the typed valueInside a form it also submits, the same as any text input.
PairThemeAPCA LcWCAG 2.2Floor
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below floor

These are the measured token pairs this component draws colour from, not a measurement of the component itself.

API reference

Prop

Type

Generated from NumberFieldProps in registry/bases/base/number-field.tsx.

value and onValueChange make this a controlled component with no internal value state. The caller stores the number, or null when the field is empty. label is required, because a spinbutton needs an accessible name. A min above max raises a development warning. min, max and step are the count's own bounds, never a clinical range.

  • ReadingInput is the control for a clinical measurement, with a unit and an out-of-range warning.
  • Field with a numeric control is plainer for a number typed once and never nudged.
  • Slider is for a value on a continuous track, where position matters more than the exact number.

On this page