A labeled numeric field with styled −/+ steppers, wired like Input.

Last updated

Uses JavaScript

Wires the −/+ buttons to the input’s native stepUp() / stepDown(), then dispatches input and change events so the value stays in sync with anything listening for it.

Without JS: The −/+ buttons are hidden (`no-js:hidden`); the field stays a plain native number input — type a value, or use the keyboard's Up/Down arrows, which still step the value natively.

Show code
<NumberInput label="Quantity" value={2} min={0} max={10} />

Add

npx @ava-ui/apogee add number-input

Pulls in Field. Ships @lucide/astro for the stepper icons. One scoped JS island (progressive enhancement) for the −/+ buttons.

Usage

---
import NumberInput from '@/components/apogee/NumberInput/NumberInput.astro'
---
<NumberInput label="Quantity" name="qty" value={1} min={0} max={99} />

API

@composable — exposed to Content Editors in AVA. Any other native <input> attribute (name, min, max, step, value, disabled, placeholder, aria-label, …) passes through to the input itself. class is applied to the outer Field wrapper, not the input group.

Prop Type Default Description
id string apogee-<random> Field id; auto-generated if omitted.
label string — Field label.
hint string — Helper text below the field (hidden when error is set).
error string — Error text; sets aria-invalid="true" on the group.
name string — Form field name.
min / max number — Bounds; the steppers stop there and a typed value is clamped on commit.
step number 1 Amount one stepper press or arrow key adds.
value number — Initial value.
size 'small' | 'medium' | 'large' | 'xlarge' 'medium' Height: 28 / 36 / 48 / 56px.
variant 'outline' | 'faded' | 'ghost' | 'headless' 'outline' The box style: bordered, tinted, bare, or no box at all.
orientation 'horizontal' | 'vertical' 'horizontal' vertical: stacked chevrons at the end of the box; the value aligns to the start.
hasError boolean false Marks the field invalid (aria-invalid, critical border) without an error message.
prefix / suffix string — Text inside the box before / after the value.
increaseLabel / decreaseLabel string 'Increase' / 'Decrease' Accessible names of the steppers (localise them).
required boolean false Marks the field required (label asterisk + native required).
class string — Extra classes on the Field wrapper, merged last.

Sizes

Show code
<NumberInput label="Small" size="small" value={1} />
<NumberInput label="Medium" value={1} />
<NumberInput label="Large" size="large" value={1} />

Orientation

Every orientation:

Show code
<NumberInput label="Seats" value={4} min={1} />
<NumberInput label="Seats" value={4} min={1} orientation="vertical" />

Hold, clamp and affixes

Hold a stepper to repeat (after 400 ms, every 60 ms; release or leave to stop). A typed value outside min / max is clamped when the field commits.

Hold + to count up; type 999 to see the clamp.

€/mo
Show code
<NumberInput label="Quantity" value={3} min={0} max={10} />
<NumberInput label="Budget" prefix="€" suffix="/mo" value={20} step={5} />

States

In USD

Show code
<NumberInput label="Amount" hint="In USD" value={10} />
<NumberInput label="Amount" error="Enter a value between 1 and 10" value={0} />
<NumberInput label="Amount" value={5} disabled />
<NumberInput label="Amount" value={1} required />

Examples

Bounded with min / max / step:

<NumberInput label="Party size" value={2} min={1} max={8} step={1} />

Unlabeled, identified for assistive tech only:

<NumberInput aria-label="Quantity" value={1} min={1} />

Accessibility

Key Action
Tab Moves focus to the input; the −/+ buttons are skipped.
↑ ↓ Step the value up / down by step (native).
  • Real <input type="number"> — typing and native validation work independent of JS.

  • The −/+ buttons carry tabindex="-1" — deliberately out of the tab order (pointer-only); keyboard users get the same result from the native arrow-key stepping, so nothing is lost.

  • label, hint, and error wire through Field: for/id, and aria-describedby pointing at the hint or error text (role="alert" on the error).

  • No JS: the −/+ buttons are hidden (no-js:hidden); the input remains a fully working native number field.

  • disabled propagates to both the <input> and the −/+ buttons — they disable together, and a disabled button can’t fire. The input also gets disabled:cursor-not-allowed.

  • prefix and suffix text is visual only; a screen reader does not read it with the field. Put units in the label or in aria-describedby.

Tokens

Token Used for
--border-neutral Group border
--background-page Group + input background
--border-primary / --outline-color-focus Focus border / focus ring (group focus-within, button focus-visible)
--background-neutral-faded-highlighted Stepper hover background
--foreground-neutral-faded Stepper icon colour (default)
--foreground-neutral Stepper icon colour (hover), input text
--background-critical Invalid border (aria-invalid)
--radius-md Corner radius

Do / Don’t

  • Do pass min / max / step so native validation matches the stepping behaviour.
  • Do use size="large" for touch-first or prominent quantity pickers.
  • Don’t add readonly — it blocks typing for no-JS and keyboard-only users, who otherwise get a fully working field.
  • Don’t omit label (or aria-label) — the field is unlabeled without one.

Content

  • Label is a noun for what’s being counted (“Quantity”, “Seats”), sentence case.
  • Hint states a constraint, not a repeat of the label (“Between 1 and 10”).
  • Error names the fix (“Enter a value between 1 and 10”).