Quantity stepper
Free · MITReact Native · ExpoA − 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.
Installation
$ npx shadcn@latest add https://goodui.dev/native/r/quantity-stepper.jsonThen 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
- 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).
- 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
| Prop | Type | Default |
|---|---|---|
| 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`). | number | 0 |
| max Highest value. Default 99. Pressing + here bounces with a warning haptic. | number | 99 |
| step Amount per press. Default 1. | number | 1 |
| 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. | number | 400 |
| disabled Dims the control and ignores presses. | boolean | false |
| haptic `false` silences this stepper's haptics. | boolean | true |
| 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.
Related
A number whose digits roll in the direction of change, with digits that open and close in place and currency signs that glide instead of jumping.
Single and range sliders whose thumbs follow the finger 1:1 and settle onto the nearest step on release, with a value bubble that grows out of the thumb while dragging and optional step ticks that tick.
Row actions that unfold with the finger; a long swipe lets the edge action take over and run, and destructive actions collapse the row before it leaves.