# NumberInput

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

URL: https://apogee.avaui.dev/docs/number-input
Last updated: 2026-10-15

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

Last updated October 15, 2026

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.

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

## Add

```sh
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

```astro
---
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

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

## Orientation

Every `orientation`:

```astro
<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.

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

## States

```astro
<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`:

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

Unlabeled, identified for assistive tech only:

```astro
<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”).

## Related

[

InputA labeled text field with hint/error messaging, wired for accessibility — plus a pure-CSS floating label.

](https://apogee.avaui.dev/docs/input)[

SliderA styled native range input with a live percentage badge.

](https://apogee.avaui.dev/docs/slider)[

FieldThe labeled-field scaffold — label, control, and hint/error — wired for accessibility.

](https://apogee.avaui.dev/docs/field)
