NumberInput
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 codeHide 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 codeHide code
<NumberInput label="Small" size="small" value={1} />
<NumberInput label="Medium" value={1} />
<NumberInput label="Large" size="large" value={1} />Orientation
Every orientation:
Show codeHide 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.
Show codeHide code
<NumberInput label="Quantity" value={3} min={0} max={10} />
<NumberInput label="Budget" prefix="€" suffix="/mo" value={20} step={5} />States
In USD
Enter a value between 1 and 10
Show codeHide 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, anderrorwire throughField:for/id, andaria-describedbypointing 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. -
disabledpropagates to both the<input>and the −/+ buttons — they disable together, and a disabled button can’t fire. The input also getsdisabled:cursor-not-allowed. -
prefixandsuffixtext is visual only; a screen reader does not read it with the field. Put units in the label or inaria-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/stepso 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(oraria-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”).