Quantity stepper

Free · MITReact Native · Expo

A − value + stepper whose value rolls in the direction of change. Hold a button to repeat with acceleration; the ends push back with a rubber-band bounce and a warning haptic. In a cart the − turns into a trash button at the minimum.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add https://goodui.dev/native/r/quantity-stepper.json

Then run npx expo install --fix so react-native-reanimated, react-native-svg match your Expo SDK. Files land in components/goodui-native/quantity-stepper.

Usage

import { useState } from "react";
import { QuantityStepper } from "@/components/goodui-native/quantity-stepper/quantity-stepper";

export function Guests() {
  const [guests, setGuests] = useState(2);
  return <QuantityStepper accessibilityLabel="Guests" value={guests} onValueChange={setGuests} min={1} max={12} />;
}

When to use

Use it for
  • Small whole numbers changed a step at a time: cart quantity, guests, tickets, rooms, reps.
  • Decimal steps with a unit, such as 0.5 kg (step and format).
  • Cart rows where reducing below one removes the item (onRemove).
Reach for something else
  • Large ranges where precision matters less than speed: use slider.
  • Free numeric entry such as an amount of money: use text-field with a decimal keyboard.
  • A choice between named options: use segmented-control.

Examples

Cart row: trash at the minimum

<QuantityStepper
  size="sm"
  accessibilityLabel="Quantity, Oat latte"
  value={item.qty}
  min={1}
  max={20}
  onValueChange={(qty) => updateQty(item.id, qty)}
  onRemove={() => removeItem(item.id)}
/>

Weight with a unit

<QuantityStepper
  defaultValue={1}
  min={0.5}
  max={10}
  step={0.5}
  format={{ style: "unit", unit: "kilogram", minimumFractionDigits: 1 }}
/>

API reference

PropTypeDefault
value
The quantity (controlled). Pair it with `onValueChange`.
number—
defaultValue
Initial quantity when uncontrolled. Defaults to `min`.
number—
onValueChange
Called with every new value, including each repeat while a button is held.
(value: number) => void—
min
Lowest value. Default 0. Pressing − here bounces with a warning haptic (or removes, see `onRemove`).
number0
max
Highest value. Default 99. Pressing + here bounces with a warning haptic.
number99
step
Amount per press. Default 1.
number1
format
Intl options for the value, for example `{ style: "unit", unit: "kilogram" }`.
Intl.NumberFormatOptions—
locale
BCP 47 locale for the value. Defaults to the GooduiLocaleProvider's language, else the device locale.
string—
size
Heights 32, 40 and 48 pt. Buttons keep a 44 pt touch target.
"sm" | "md" | "lg""md"
variant
`filled` sits on a muted pill; `outline` draws a hairline pill for busy surfaces.
"filled" | "outline""filled"
onRemove
Cart pattern: at `min` the − button turns into a trash button that calls this, instead of bouncing.
() => void—
repeatDelay
Hold a button this long (ms) before it starts repeating. Default 400.
number400
disabled
Dims the control and ignores presses.
booleanfalse
haptic
`false` silences this stepper's haptics.
booleantrue
accessibilityLabel
What is being counted, for screen readers: "Guests", "Quantity, Oat latte". Same as `labels.quantity`.
string—
labels
Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide.
Partial<QuantityStepperLabels>—
style
Style for the outer container.
StyleProp<ViewStyle>—
testID
Identifier for end-to-end tests.
string—

Motion and haptics

  • The value rolls up or down with rolling-number, digit by digit.
  • Press: the icon scales to 0.84 and a soft fill appears behind it from touch down.
  • At a bound the value nudges toward the pressed side and springs back on the bouncy spring; the button at that end dims.
  • With onRemove, the − turns away and a red trash turns in on the morph spring when the value reaches min.
  • Reduced motion: no press scale and no nudge; the trash crossfades; digits change without rolling.
  • Haptic: tick on each step, at most one every 90 ms while a held button accelerates
  • Haptic: warning when a press hits min or max
  • Haptic: delete when the trash button removes the item
  • Repeats run on a JS timer that starts at 170 ms and accelerates to 40 ms per step; each step is one React update.
  • Press, dim, nudge and icon morph animate on the UI thread.

Accessibility

  • The whole stepper is one adjustable element (role adjustable, a slider on the web) with min, max, now and the formatted text as its value.
  • Screen readers use the increment and decrement actions (swipe up and down on iOS); with onRemove at the minimum a 'Remove' action is added.
  • On the web it is focusable and arrow keys or + and − step it.
  • The two buttons are hidden from screen readers so the value is not read three times. Each keeps a 44 pt touch target through hitSlop.
  • On the web it is a spinbutton with aria-valuenow, min, max and valuetext; arrow keys and + / - step.

Notes for AI

  • Pass accessibilityLabel naming what is counted ('Guests', 'Quantity, Oat latte').
  • A tap steps once on release; holding past repeatDelay (400 ms) repeats. Do not add your own long-press handling.
  • onRemove changes the minimum's − button into remove. Without it, − at min bounces.
  • For decimal steps set step (0.5) and format; values are rounded to the step's precision.