<!-- Protocore Design System — NumberInput -->
# NumberInput

- **Category:** Inputs (`inputs`)
- **Slug:** `inputs/number-input`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { NumberInput } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/inputs/number-input

> Numeric field with a mono tabular value and a hairline stepper column — clamps to min/max and steps with the arrow keys.

## When to use

Use **NumberInput** for a *typed* number where nudging by a step is useful — replica counts, timeouts, gas prices, quantities.

- Picking a value from a bounded continuous range where the exact number matters less than the feel? Use a **Slider**.
- Just *displaying* a formatted currency figure? Use **MoneyAmount** — NumberInput is for editing, not read-only presentation.
- Free-form text that happens to contain digits (an ID, a version)? Use a plain **Input**.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `className` | `string` | no | — | — |
| `defaultValue` | `number` | no | — | Initial value in uncontrolled mode. |
| `invalid` | `boolean` | no | — | Mark the field invalid — danger border plus `aria-invalid`. |
| `max` | `number` | no | — | Upper bound — clamped on blur/step; disables the + button at the ceiling. |
| `min` | `number` | no | — | Lower bound — clamped on blur/step; disables the − button at the floor. |
| `onValueChange` | `((value: number) => void)` | no | — | Fires with the parsed number on edit and on step. |
| `precision` | `number` | no | — | Fixed decimal places to display and round to on commit. |
| `size` | `enum` | no | `md` | Control height: `sm` (32) · `md` (36, default) · `lg` (40). |
| `step` | `number` | no | `1` | Step increment for the buttons and ArrowUp/Down. |
| `style` | `CSSProperties` | no | — | — |
| `value` | `number` | no | — | Controlled numeric value. `undefined` = empty field. |

## Examples

### Basics

A numeric control that renders the value in mono tabular figures with a +/- stepper column. It reports parsed numbers through `onValueChange`; give it an `aria-label` (or wrap it in a Field) so the spinbutton is named.

```tsx
import { NumberInput } from "@protocore/pds";

export default function Basics() {
  // aria-label names the spinbutton; ArrowUp/ArrowDown step the value.
  return (
    <NumberInput aria-label="Replica count" defaultValue={3} min={0} style={{ maxWidth: 160 }} />
  );
}
```

### Bounds and step

`min`, `max`, and `step` govern both the arrow keys and the stepper buttons. Values clamp on step and on blur, and each stepper button disables when the value hits its edge.

```tsx
import { NumberInput } from "@protocore/pds";
import { useState } from "react";

export default function Bounds() {
  const [replicas, setReplicas] = useState(3);

  // min/max clamp on step and blur; the stepper buttons disable at the edges.
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
      <NumberInput
        aria-label="Replicas"
        value={replicas}
        onValueChange={setReplicas}
        min={1}
        max={9}
        step={1}
        style={{ maxWidth: 160 }}
      />
      <span style={{ font: "13px sans-serif", opacity: 0.7 }}>{replicas} of 9 max</span>
    </div>
  );
}
```

## Do & don't

**Do**

- Set min/max so the value can't leave its valid range.
- Use step (and a fractional step with precision) to match the quantity's granularity.
- Give it an aria-label or wrap it in a Field so the spinbutton is named.
- Use precision for money- and rate-style values that need fixed decimals.

**Don't**

- Don't use it for a continuous, feel-based range — that's a Slider.
- Don't use it to display read-only currency — that's MoneyAmount.
- Don't leave a bounded value unclamped; set min/max rather than validating after.
- Don't ship it unnamed — a bare spinbutton has no accessible label.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `ArrowUp` | Increment by `step` (clamped to `max`). |
| `ArrowDown` | Decrement by `step` (clamped to `min`). |
| `Type` | Edit the raw value; it parses and clamps on blur. |

**Notes**

- The field is a `role="spinbutton"` exposing `aria-valuenow`, `aria-valuemin`, and `aria-valuemax`.
- Give it an `aria-label` or a Field label — a spinbutton needs an accessible name.
- The +/- stepper buttons are `aria-hidden` and out of the tab order; keyboard users step with the arrows.
- `invalid` sets `aria-invalid` and the danger border.

## Related

`input`, `field`, `slider`, `money-amount`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
