Unified UI

Number Input

A numeric stepper input with increment/decrement buttons, keyboard navigation, min/max clamping, and animated digit transitions.

Basic

A production-ready numeric stepper with +/− buttons, keyboard navigation, min/max clamping, precision control, and animated digit roll transitions.

Installation

Install the component via the CLI in one command.

npx @work-rjkashyap/unified-ui add number-input
pnpm dlx @work-rjkashyap/unified-ui add number-input
npx @work-rjkashyap/unified-ui add number-input
bunx @work-rjkashyap/unified-ui add number-input

If you haven't initialized your project yet, run npx @work-rjkashyap/unified-ui init first. See the CLI docs for details.

Or install the full package

Use this approach if you prefer to install the entire design system as a dependency instead of copying individual components.

npm install @work-rjkashyap/unified-ui
pnpm add @work-rjkashyap/unified-ui
yarn add @work-rjkashyap/unified-ui
bun add @work-rjkashyap/unified-ui

Anatomy

import { NumberInput } from "@work-rjkashyap/unified-ui";

Basic Usage

NumberInput works uncontrolled with defaultValue, or controlled with value + onChange.

{
	/* Uncontrolled */
}
<NumberInput defaultValue={10} min={0} max={100} />;

{
	/* Controlled */
}
const [qty, setQty] = useState(1);
<NumberInput value={qty} onChange={setQty} min={1} max={50} />;

Sizes

SizeHeightUse Case
sm32pxCompact forms, table cells
md36pxDefault — most form contexts
lg40pxProminent inputs

Variants

Step

Control the increment/decrement amount with step. Defaults to 1.

Hold Shift while pressing arrow keys to step by 10× the current step value.

Precision (Decimals)

Use precision to allow and display decimal values. Pair with step for consistent increments.

Custom Format

Supply a formatValue function to display the number with a prefix, suffix, or custom notation.

Disabled & Read-Only

{
	/* Disabled — no interaction */
}
<NumberInput defaultValue={42} disabled />;

{
	/* Read-only — visible but not editable */
}
<NumberInput defaultValue={42} readOnly />;

Controlled

const [quantity, setQuantity] = useState(1);

<div className="flex flex-col gap-2">
	<label className="text-sm font-medium">Quantity</label>
	<NumberInput
		value={quantity}
		onChange={setQuantity}
		min={1}
		max={99}
		step={1}
		aria-label="Quantity"
	/>
	<p className="text-xs text-muted-foreground">
		Total: ${(quantity * 29.99).toFixed(2)}
	</p>
</div>;

In a Form

A typical quantity selector inside a product form.

<form className="flex flex-col gap-4">
	<div className="flex flex-col gap-1.5">
		<label className="text-sm font-medium">Quantity</label>
		<NumberInput
			defaultValue={1}
			min={1}
			max={99}
			aria-label="Item quantity"
		/>
	</div>

	<div className="flex flex-col gap-1.5">
		<label className="text-sm font-medium">Discount (%)</label>
		<NumberInput
			defaultValue={0}
			min={0}
			max={100}
			step={5}
			formatValue={(v) => `${v}%`}
			aria-label="Discount percentage"
		/>
	</div>
</form>

Props

PropTypeDefaultDescription
valuenumberControlled value.
defaultValuenumber0Initial value for uncontrolled mode.
onChange(value: number) => voidCallback fired when the value changes.
minnumberMinimum allowed value.
maxnumberMaximum allowed value.
stepnumber1Increment/decrement amount per step.
precisionnumber0Number of decimal places to display and allow.
variant"default" | "primary""default"Visual variant.
size"sm" | "md" | "lg""md"Size variant.
disabledbooleanfalseDisables all interaction.
readOnlybooleanfalsePrevents value changes while keeping it visible.
formatValue(value: number) => stringv.toFixed(n)Custom display format function.
parseValue(raw: string) => numberCustom parse function for direct text input.
incrementLabelstring"Increment"Accessible label for the + button.
decrementLabelstring"Decrement"Accessible label for the − button.
aria-labelstringAccessible label for the spinbutton container.
classNamestringAdditional CSS classes on the container.

Keyboard Navigation

KeyBehavior
Arrow UpIncrease by one step
Arrow DownDecrease by one step
Shift + ↑Increase by 10× the step
Shift + ↓Decrease by 10× the step
HomeSet to minimum value
EndSet to maximum value
Enter / ClickOpens direct text input mode
Enter (in edit)Commits the typed value
Escape (edit)Cancels editing and restores previous value

Motion

The value display uses the numberRoll preset — digits animate with a vertical slide (y: 6px → 0, opacity: 0 → 1) on each change via AnimatePresence. This gives a tactile "odometer" feel. The animation is skipped when prefers-reduced-motion is active.

Accessibility

  • Root element uses role="spinbutton" with aria-valuenow, aria-valuemin, aria-valuemax, aria-disabled, and aria-readonly.
  • + and buttons have descriptive aria-label attributes.
  • The value display button announces "Current value: X, press to edit" for screen readers.
  • In edit mode, the <input> uses inputMode="decimal" for mobile numeric keyboards.
  • Min/max clamping is enforced on blur — invalid entries are silently corrected.
  • All keyboard interactions are WCAG AA compliant for spinbutton role.

Design Tokens

TokenUsage
--inputBorder color (default variant)
--primaryBorder color (primary variant)
--ringFocus ring color
--accentStepper button hover background
--radius-mdContainer border radius
--duration-fastTransition speed

On this page