Skip to content
WANCRAFT

A.65Buttons & Controls

Number Stepper

A numeric field with − / + buttons, accelerating hold-to-repeat, rolling digits and an elastic bump at the limits.

NewReactMotionTailwindadvanced

Overview

Number Stepper is a quantity, price or unit input that feels mechanical. Pressing − or + steps once; holding repeats after a short delay and speeds up the longer you hold. Digits roll individually in the direction of change, and pushing past min or max gives a short elastic bump instead of silently doing nothing. The text field is a role="spinbutton" with the full keyboard model (arrows, Page Up / Down, Home / End), accepts typed values that are validated, clamped and rounded on blur or Enter, and formats with a prefix, a suffix or your own formatter.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/number-stepper.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/number-stepper.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/number-stepper.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { NumberStepper } from "@/components/ui/number-stepper";

export function NumberStepperExample() {
  return (
    <NumberStepper
      defaultValue={2}
      onValueChange={(value) => console.log(value)}
    />
  );
}

03Props

PropTypeDefaultDescription
labelstring"Quantity"Visible label and accessible name of the spinbutton.
hideLabelbooleanfalseKeep the label for assistive technology only.
value / defaultValuenumber— / 0Controlled value, or the initial value when uncontrolled.
onValueChange(value: number) => void—Called with every committed value (steps, keys, typed entry).
min / max / stepnumber0 / 100 / 1Range and step. Values are clamped and rounded to the precision.
largeStepnumber10 × stepStep for Page Up / Page Down.
precisionnumberdecimals of stepFraction digits shown and kept.
prefix / suffixstring"" / ""Text around the number, e.g. "$" or " kg".
formatValue(value: number) => string—Full formatter; overrides prefix, suffix and locale. Also used for aria-valuetext.
localestring"en-US"Locale for digit grouping.
accentstring"#FF7448"Focus border and ring colour.
size"sm" | "md" | "lg""md"Height, button width and type size.
disabled / name / inputIdboolean / string / string—Disable input; submit via a hidden input; set the input id.
...propsHTMLAttributes<HTMLDivElement>—Forwarded to the root element.

04Features

  • 01Press-and-hold auto-repeat that accelerates from 140ms to 28ms between steps
  • 02Digit-by-digit rolling display that moves up or down with the change
  • 03Elastic spring bump when you push past min or max (buttons, keys or a rejected entry)
  • 04Typed entry with validation on blur / Enter: invalid text reverts, out-of-range values clamp
  • 05Prefix / suffix or a custom formatter; precision derived from the step
  • 06Controlled or uncontrolled, with an optional hidden input for forms
motion · springinput · pressinput · clickinput · keyboardinput · focus

05Accessibility

  • The field is a native text input (inputMode decimal / numeric) with role="spinbutton", aria-valuenow, aria-valuemin, aria-valuemax and aria-valuetext including prefix / suffix.
  • Arrow Up / Down step, Page Up / Down take a large step, Home / End jump to min / max, Enter commits and Escape cancels typing.
  • The − / + buttons are labelled and linked with aria-controls; following the WAI-ARIA spinbutton pattern they are out of the tab order but still work with assistive-technology activation.
  • Rejected or adjusted entries set aria-invalid and are explained in a polite live region.
  • With prefers-reduced-motion the digits change instantly and there is no bump.

06Limitations

  • Typed entry accepts a dot as the decimal separator; grouping characters, prefix and suffix are stripped before parsing.