# Number Input

An accessible React number input for Next.js and Tailwind with step buttons, keyboard control, min and max, and Intl number formatting.

Source: https://ui.fewtone.com/components/number-input

## Installation

```bash
npx shadcn@latest add https://ui.fewtone.com/r/number-input.json
```

## Usage

```tsx
import { NumberInput } from "@/components/ui/number-input"

export function Quantity() {
  return (
    <>
      <label htmlFor="quantity">Quantity</label>
      <NumberInput id="quantity" defaultValue={1} min={1} max={99} />
    </>
  )
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `number \| null` | – | The number, when you control it. null is an empty field. |
| `defaultValue` | `number \| null` | `null` | The number at first. |
| `onValueChange` | `(value: number \| null) => void` | – | Called with the new number after a step, after typing a number in range, or when you leave the field. |
| `min` | `number` | – | The lowest number. Home goes here. |
| `max` | `number` | – | The highest number. End goes here. |
| `step` | `number` | `1` | How much the arrow keys and the buttons change it. |
| `largeStep` | `number` | `step × 10` | How much Page Up and Page Down change it. |
| `format` | `Intl.NumberFormatOptions` | – | How the number is shown, for example { style: "currency", currency: "USD" }. Typed text is read back the same way. |
| `locale` | `string` | – | The locale for formatting and reading numbers. Set it when you render on the server, so the server and the browser agree. |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | 32, 36 or 40px tall, matching Input. |
| `name` | `string` | – | Submits the plain number with a form, through a hidden input. |
| `incrementLabel` | `string` | `"Increase"` | The plus button's accessible name. |
| `decrementLabel` | `string` | `"Decrease"` | The minus button's accessible name. |
| `aria-invalid` | `boolean` | – | Marks the number as wrong: the border and focus ring turn destructive. |
| `disabled` | `boolean` | `false` | Prevents changes. |
| `readOnly` | `boolean` | `false` | Shows the number without letting it change. |

**When to use it:** Use NumberInput for numbers people type or nudge: quantities, prices, percentages, counts. Use Slider when the exact number matters less than its place in a range, and Input for numbers that aren't amounts, like phone or card numbers.
**Built on:** Button, Intl.NumberFormat.
**Accessibility pattern:** https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/
**npm packages (installed by the CLI):** class-variance-authority@^0.7.1, lucide-react@^1.52.0, motion@^14.0.0.
**Installs these FewTone UI components with it:** `button`.

## Examples

### Quantity and price

Step with the buttons or the arrow keys and the number rolls: the digits that change lift away as the new ones rise in, upwards for up and downwards for down. Hold a button to keep stepping. The price is formatted with Intl.NumberFormat.

```tsx
"use client";

import { NumberInput } from "@/components/ui/number-input";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { useId, useState } from "react";

type Size = "sm" | "md" | "lg";

/*
 * A quantity and a price side by side, named underneath, with the size in the top-right corner. An empty
 * row at the bottom, as tall as the setting's row, keeps them centred.
 */
export default function NumberInputFields() {
  const id = useId();
  const [size, setSize] = useState<Size>("md");

  return (
    <div className="flex w-full flex-col gap-6 self-stretch">
      <div className="-mt-6 -mr-6 flex items-center justify-end gap-2">
        <label htmlFor={`${id}-size`} className="text-sm text-muted-foreground">
          Size
        </label>
        <Select value={size} onValueChange={(value) => setSize(value as Size)}>
          <SelectTrigger id={`${id}-size`} size="sm" className="w-28">
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="sm">Small</SelectItem>
            <SelectItem value="md">Medium</SelectItem>
            <SelectItem value="lg">Large</SelectItem>
          </SelectContent>
        </Select>
      </div>

      <ul className="flex flex-1 flex-wrap items-center justify-center gap-x-10 gap-y-6">
        <li className="flex flex-col items-center gap-2">
          <NumberInput aria-label="Quantity" defaultValue={1} min={1} max={99} size={size} className="w-32" />
          <span className="text-xs text-muted-foreground">Quantity</span>
        </li>
        <li className="flex flex-col items-center gap-2">
          <NumberInput
            aria-label="Price"
            defaultValue={24}
            min={0}
            step={0.5}
            largeStep={10}
            locale="en-US"
            format={{ style: "currency", currency: "USD" }}
            size={size}
            className="w-40"
          />
          <span className="text-xs text-muted-foreground">Price</span>
        </li>
      </ul>

      <div aria-hidden className="-mb-6 h-(--size-control-sm)" />
    </div>
  );
}
```

### Percent

A value from 0 to 1 shown as a percent, in steps of 5%. Page Up and Page Down take bigger steps; Home and End go to the ends.

```tsx
import { NumberInput } from "@/components/ui/number-input";
import { useId } from "react";

/** A percent between 0 and 1 in steps of 5%. Home and End go to 0% and 100%. */
export default function NumberInputPercent() {
  const id = useId();
  return (
    <div className="grid w-full max-w-xs gap-2">
      <label htmlFor={id} className="text-sm font-medium">
        Discount
      </label>
      <NumberInput
        id={id}
        defaultValue={0.15}
        min={0}
        max={1}
        step={0.05}
        largeStep={0.25}
        locale="en-US"
        format={{ style: "percent" }}
        aria-describedby={`${id}-hint`}
      />
      <p id={`${id}-hint`} className="text-sm text-muted-foreground">
        Up to 100%, in steps of 5%.
      </p>
    </div>
  );
}
```

### Disabled

```tsx
import { NumberInput } from "@/components/ui/number-input";
import { useId } from "react";

export default function NumberInputDisabled() {
  const id = useId();
  return (
    <div className="grid w-full max-w-xs gap-2">
      <label htmlFor={id} className="text-sm font-medium">
        Seats
      </label>
      <NumberInput id={id} defaultValue={5} min={1} disabled />
    </div>
  );
}
```

## Related

- [Input](https://ui.fewtone.com/components/input.md): An accessible React text input with three sizes and clear invalid, disabled and file states, built on a native input.
- [Slider](https://ui.fewtone.com/components/slider.md): An accessible React slider for Next.js and Tailwind: one value or a range, with a thumb that grows into a pill showing its value as you reach for it.
- [Button](https://ui.fewtone.com/components/button.md): An accessible React button for Next.js and Tailwind CSS with six variants, three sizes, icons and a soft tint on hover and press, built on a native button.
