Number Input

A number field with steppers. Uses type="text" with inputMode="decimal", because the native number input silently discards what it cannot parse — a half-typed "1e" or "-" vanishes as you type it.

src/components/ui/number-input.tsx

About

From the component's own source, where it was written.

A number field with steppers.

inputMode="decimal" with type="text", not type="number". The native number input silently discards what it cannot parse, so a half-typed "1e" or "-" vanishes as you type it; it also exposes a spinner that cannot be styled and scrolls the value on wheel over the field. Keeping the text input means the caller sees every keystroke and the value is parsed on commit.

Clamping happens on blur rather than on each keystroke, so typing "5" on the way to "50" in a field with a minimum of 10 is not rewritten under the cursor.

Install

npx astralyx-ui add number-input

Usage

import { NumberInput } from '@/components/ui/number-input'

<NumberInput value={qty} onValueChange={setQty} min={1} max={99} suffix="items" />

Composer

Change the props and the source below follows.

items
Props
tsx
<NumberInput
  defaultValue={12}
  min={0}
  max={99}
  step={1}
  size="md"
  suffix="items"
/>

Examples

Sizes and affixes

API

Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.

NumberInput

A number field with steppers.

Extends Omit<ComponentProps<'input'>, 'value' | 'defaultValue' | 'onChange' | 'size' | 'type' | 'prefix'>.

PropTypeDefaultDescription
maxnumber | string—Clamped on blur, not per keystroke — rewriting "5" to "10" mid-type fights someone entering "50".
minnumber | string—Clamped on blur, not per keystroke — rewriting "5" to "10" mid-type fights someone entering "50".
stepnumber | string1Precision is inferred from the step, so 0.1 stays at one decimal instead of accumulating float noise.
valuenumber—Reports undefined while the field is empty or unparseable, rather than coercing to 0.
defaultValuenumber—Reports undefined while the field is empty or unparseable, rather than coercing to 0.
onValueChange(value: number) => void—Reports undefined while the field is empty or unparseable, rather than coercing to 0.
precisionnumber1Precision is inferred from the step, so 0.1 stays at one decimal instead of accumulating float noise.
sizekeyof typeof fieldSize'md'—
variant'default' | 'secondary' | 'ghost''default'—
errorbooleanfalse—
prefixstring—Static affixes inside the field.
suffixstring—Static affixes inside the field.

Behaviour

TopicDetailDescription
keyboard↑ ↓Arrow keys step. The steppers are out of the tab order, since the field itself already does this.