Components / forms

Spinbutton

Building the steppers as real buttons was rejected: three tab stops for one value.

Rendered from the package · open on its own

Summary

A number the reader types or nudges — quantities, dimensions, durations.

APG patternSpinbutton
Built onnative elements
Statusstable
Since0.1.0
<Spinbutton id="width" label="Width" defaultValue={12} min={1} max={24} suffix="col" />

Anatomy

A native number input with a stepper pair beside it.

PartElementRequired
control<input>yes
step<button>yes
steppers<span>yes

Properties

PropertyValuesDefaultWhat it does
valuenumber | stringnonePassing this makes the input controlled.
defaultValuenumber | stringnoneThe starting number when the input keeps its own state.
onValueChange(value: number | null) => voidnoneFires with the parsed number, or null when the field is cleared.
minnumbernoneThe lowest accepted value. Enforced by the platform, including on the arrow keys.
maxnumbernoneThe highest accepted value.
stepnumber1How far one arrow key moves. It also decides what the platform treats as a valid number — a step of 1 rejects 2.5.
invalidbooleannoneMarks the value as failing validation. Pair it with a visible message: the red border alone fails roughly one man in twelve.
idstringnoneWires the label to the input. One is generated when omitted.

States

StateExpressed as
Disabled:disabled
Focused:focus-visible
Hover:hover
Invaliddata-invalid

Behavior

The input is a native type="number", which is the point. It already has arrow keys, Home and End, typing, locale parsing and the numeric keyboard on touch. Every custom number editor rebuilds those and most rebuild them badly.

Accessibility

The steppers are aria-hidden and out of the tab order. They duplicate an operation the input already exposes to the keyboard, and exposing them again would make a screen-reader user tab through three controls to set one value.

The suffix is part of the label, not a decoration beside it. A reader hearing "12" needs "columns".

Keyboard

KeyResult
Arrow Up / Arrow DownIncrement and decrement by step — from the native input
Home / EndJump to min or max
Page Up / Page DownMove by a larger increment
typingEnters a value directly, parsed by the platform in the reader's locale

Roles, states and properties

AttributeSet by
aria-hiddenalways true
aria-invalidset at render

Focus

One tab stop: the input. The steppers are aria-hidden and out of the tab order — they duplicate the arrow keys, and exposing them would make a screen-reader user pass three controls to set one value.

Screen reader

Not recorded as verified. Stage 4 of the readiness ladder — driving each component with VoiceOver and NVDA — is unstarted across the package, and no component should claim otherwise until docs/journal/ records the session that did it.

Contrast and target size

npm run check:contrast audits every color pair this component uses across all four skins, light and dark, and fails the build on a shortfall. Two waivers are on the record in docs/audit/DEFERRED.md.

It takes the touch floor under (pointer: coarse) — the visual size is unchanged on a desktop pointer.

Tested

  • test/apg/batch-b.test.jsx
  • test/apg/partials-sweep.test.jsx
  • test/browser/upstream-forms-menubar.spec.mjs

Known gaps

  • 7 upstream refs — no datepicker component
  • 4 upstream refs — APG's own page or markup, not the pattern

Appearance

TokenUsed for
--se-accentborder-color
--se-border-hoverborder-color
--se-border-strongborder
--se-border-widthborder
--se-control-hblock-size, inline-size
--se-duration-instanttransition
--se-ease-standardtransition
--se-focus-coloroutline
--se-focus-widthoutline, outline-offset
--se-font-codefont-family
--se-icon-smblock-size, inline-size
--se-leading-uiline-height
--se-negativeborder-color
--se-radius-mdborder-end-end-radius, border-end-start-radius, border-start-end-radius, border-start-start-radius
--se-space-3padding-inline
--se-surfacebackground
--se-surface-sunkbackground
--se-target-minmin-block-size
--se-textcolor
--se-text-disabledcolor
--se-text-mutedcolor
--se-text-smfont-size

The value uses --se-font-code for tabular figures, so a number does not change width as it changes value.

Motion

None.

Content

The label names the quantity and the suffix names the unit. If the unit only makes sense in context — col for columns — spell it in the accessible name rather than the visible one.

Usage

Use it when the number is exact and small-range. For a value where the precise figure barely matters, a Slider reads faster.

When not to use it. For a number with no meaningful step — a price the reader types freely is a Field with a text input, and steppers on it are a control nobody will press.

Notes

Building the steppers as real buttons was rejected: three tab stops for one value.