ns-ui

Stepper Ratchet

Numeric spinbutton built as a ratchet and pawl: incrementing is free and clicky and a hold repeats and accelerates, but decrementing requires holding ~250ms while the pawl visibly swings 35 degrees clear before a hold starts stepping back, also accelerating.

Use when a quantity stepper for a CONSEQUENTIAL value (seats, licenses, allocations) where accidental decrement is costly: incrementing is a free, instant press (and a hold repeats, accelerating from 400ms toward 60ms per step), but decrementing needs a ~250ms pointer hold before the same accelerating repeat starts running backward — direction-dependent friction that makes 'down is deliberate' legible without a confirm dialog. This is a continuing, held control with per-step asymmetric resistance, not a single time-gated commit (see confirm-hold-ink) and not a value-indicating gauge (see stepper-needle, whose stepper is symmetric in both directions).

Install

npx shadcn add https://design.helpmarq.com/r/stepper-ratchet.json
  • stepper
  • spinbutton
  • input
  • svg
  • micro-interaction
  • form

ns-ui / stepper-ratchet — up is free, down needs a hold

Workspace seats

Billed monthly at $24/seat. Removing a seat revokes access immediately, so it takes a deliberate hold.

Seats150
6
6 seats$144/mo

GPU allocation

Units reserved for this project. Tap + to grab more; hold − and the pawl has to swing clear before it lets go of one.

Allocation0200
40units

Press + to add — instant, no arm delay, and holding it repeats, accelerating from 400ms toward 60ms between steps. Press and hold − instead: the pawl rotates clear over a quarter second before it starts giving anything back, then repeats on that same accelerating schedule. Both work with mouse or touch, and each step pulses a quick vibration where the device supports it (a no-op on this Mac). Focus the number and use Arrow Up/Down: both directions step immediately, no hold required.

Props

PropTypeDefaultDescription
value?numbercontrolled value; omit for uncontrolled
defaultValue?number
min?number
max?number
step?number
label?stringaccessible name for the spinbutton, and the on-screen caption
unit?stringunit suffix rendered beside the value, e.g. "seats"
onValueChange?(value: number) => void
disabled?boolean
className?string