# Quantity stepper

> 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.

- Type: Component (inputs)
- Access: Free, open source (MIT)
- Page: https://goodui.dev/native/components/quantity-stepper
- Markdown: https://goodui.dev/native/components/quantity-stepper.md
- Registry item: https://goodui.dev/native/r/quantity-stepper.json
- Dependencies: react-native-reanimated, react-native-svg
- Keywords: react native stepper, quantity selector, plus minus counter, cart quantity, guest counter, number stepper hold to repeat

## 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).

## When not to use

- 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.

## Installation

With the shadcn CLI (needs a components.json in the Expo app), then `npx expo install --fix` so native dependencies match your Expo SDK. The foundation comes along the first time.

```bash
npx shadcn@latest add https://goodui.dev/native/r/quantity-stepper.json
npx expo install --fix
```

The component lands in `components/goodui-native/quantity-stepper/` and imports the foundation from `components/goodui-native/foundation/` with relative paths; it also installs locale, rolling-number.

Built-in strings follow `<GooduiLocaleProvider>` (one [language pack](https://goodui.dev/native/r/locale-tr.json) translates the whole kit); the `labels` prop overrides them for one instance.

## Usage

```tsx
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} />;
}
```

## Examples

### Cart row: trash at the minimum

```tsx
<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

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

## API reference

### QuantityStepper

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

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

## 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.

## Motion

- 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.

## Haptics

- tick on each step, at most one every 90 ms while a held button accelerates
- warning when a press hits min or max
- delete when the trash button removes the item

## Performance

- 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.

## 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

- [Rolling number](https://goodui.dev/native/components/rolling-number.md): 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.
- [Slider](https://goodui.dev/native/components/slider.md): 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.
- [Swipe actions](https://goodui.dev/native/components/swipe-actions.md): 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.

## Guidance for AI tools

Quantity stepper: 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. Use only the documented props. Keep accessibility, reduced motion and both color schemes intact when adapting it.

Full library index: https://goodui.dev/native/llms.txt
