NumberInput

A simple counter component with increment and decrement buttons

Installation

pnpm add @wandercom/design-system-web

Usage

15 lines
import { NumberInput } from '@wandercom/design-system-web/ui/number-input';

export function Example() {
  const [value, setValue] = useState<number | null>(5);

  return (
    <NumberInput
      value={value}
      min={0}
      max={10}
      step={1}
      onChange={setValue}
    />
  );
}

Examples

Default

Basic number input with increment and decrement buttons.

Loading example...

With placeholder

Number input displaying a placeholder when value is null.

Loading example...
10 lines
const [value, setValue] = useState<number | null>(null);

<NumberInput
  value={value}
  min={0}
  max={100}
  step={5}
  placeholder="Any"
  onChange={setValue}
/>

With currency

Number input with currency symbol prefix.

Loading example...
10 lines
const [value, setValue] = useState<number | null>(50);

<NumberInput
  value={value}
  min={0}
  max={500}
  step={25}
  currency="USD"
  onChange={setValue}
/>

Custom placeholder

Number input with a custom placeholder text.

Loading example...
10 lines
const [value, setValue] = useState<number | null>(null);

<NumberInput
  value={value}
  min={1}
  max={10}
  step={1}
  placeholder="None"
  onChange={setValue}
/>

International formatting

Number input demonstrating various currency and locale combinations with proper formatting:

  • United States (en-US): $1,500 (comma separator, symbol before)
  • Germany (de-DE): 1.500 € (period separator, symbol after)
  • United Kingdom (en-GB): £1,500 (comma separator, symbol before)
  • Japan (ja-JP): Â¥1,500 (comma separator, symbol before)
Loading example...
61 lines
import { Label } from '@wandercom/design-system-web/ui/label';

const [usValue, setUsValue] = useState<number | null>(1500);
const [deValue, setDeValue] = useState<number | null>(1500);
const [gbValue, setGbValue] = useState<number | null>(1500);
const [jpValue, setJpValue] = useState<number | null>(1500);

<div className="flex flex-col gap-4">
  <div className="flex flex-col gap-2">
    <Label htmlFor="us-input">United States (en-US)</Label>
    <NumberInput
      id="us-input"
      value={usValue}
      min={0}
      max={10000}
      step={100}
      currency="USD"
      locale="en-US"
      onChange={setUsValue}
    />
  </div>
  <div className="flex flex-col gap-2">
    <Label htmlFor="de-input">Germany (de-DE)</Label>
    <NumberInput
      id="de-input"
      value={deValue}
      min={0}
      max={10000}
      step={100}
      currency="EUR"
      locale="de-DE"
      onChange={setDeValue}
    />
  </div>
  <div className="flex flex-col gap-2">
    <Label htmlFor="gb-input">United Kingdom (en-GB)</Label>
    <NumberInput
      id="gb-input"
      value={gbValue}
      min={0}
      max={10000}
      step={100}
      currency="GBP"
      locale="en-GB"
      onChange={setGbValue}
    />
  </div>
  <div className="flex flex-col gap-2">
    <Label htmlFor="jp-input">Japan (ja-JP)</Label>
    <NumberInput
      id="jp-input"
      value={jpValue}
      min={0}
      max={10000}
      step={100}
      currency="JPY"
      locale="ja-JP"
      onChange={setJpValue}
    />
  </div>
</div>

Props

value?:

number | null
The current value of the input. When null, displays the placeholder. Defaults to null.

min?:

number
The minimum allowed value. Defaults to 0.

max?:

number
The maximum allowed value. Defaults to 100.

step?:

number
The increment/decrement step value. Defaults to 1.

placeholder?:

string
Text displayed when value is null. Defaults to "Any".

currency?:

string
Currency symbol or ISO 4217 code (e.g., "$", "USD", "EUR"). Displays before the value.

locale?:

string
BCP 47 locale tag for number formatting (e.g., "en-US", "de-DE", "fr-FR"). Defaults to "en-US".

onChange?:

(value: number | null) => void
Callback fired when the value changes via the increment/decrement buttons.

disabled?:

boolean
Whether the input is disabled. Defaults to false.

required?:

boolean
Whether the input is required in forms. Defaults to false.

aria-label?:

string
Accessible label for the stepper group.

aria-labelledby?:

string
ID of the element that labels the stepper group.

aria-describedby?:

string
ID of the element that describes the value.

id?:

string
Optional ID for the value element. Auto-generated if not provided.

name?:

string
Form field name. When provided, renders a hidden input for form submission.

decreaseLabel?:

string
Accessible label for the decrement button. Defaults to "Decrease value".

increaseLabel?:

string
Accessible label for the increment button. Defaults to "Increase value".

className?:

string
Additional CSS classes to apply to the container.

Accessibility

The − and + buttons are the only interactive controls. The displayed value is a non-interactive element with aria-live="polite" so screen readers announce changes as the user steps the value.

Keyboard Navigation:

  • Tab - Move focus between the − and + buttons
  • Enter / Space - Activate the focused button

Screen Reader Support:

  • Stepper group is labeled via aria-label / aria-labelledby
  • Value updates are announced via aria-live="polite"
  • Each button has a descriptive aria-label ("Decrease value" / "Increase value")

Form Integration:

  • Optional hidden input for native form submission when name prop is provided
  • Supports required and disabled states

Behavior

Increment/decrement buttons are the only way to change the value. They adjust by the step amount, respecting min and max bounds. When the value is null (placeholder state), pressing increment sets the value to min rather than min + step.

Buttons are disabled when the value reaches the minimum or maximum limit. The decrement button is also disabled while the value is null.

Placeholder display shows custom text when value is null, useful for "Any" or "None" states.

Values are automatically clamped to stay within the min and max range.

International formatting uses Intl.NumberFormat to display numbers with locale-specific thousands separators and currency symbols.

NumberInput