suiss/uim · Interaction

Stepper

A number input with minus and plus buttons that step the value, repeat while held and play a selection haptic.

Stepper is an open-source mobile interaction from suiss/uim for React Native and Expo, with SwiftUI and Jetpack Compose versions built from the same design tokens. The @esuiss/uim CLI copies its source into your project, so you own the code.

iOS
Stepper on iOS in light mode

Stepper is a number field laid out as a right-aligned value followed by a field holding minus and plus buttons, inset from its edges. Each step snaps to step and stays within min and max, and a button that can go no further is shown as disabled.

Pressing and holding a button steps once, then repeats every 60 ms after 400 ms. The value can also be typed. Use it for quantities, copies or percentages in settings rows.

Installation

$ pnpm dlx @esuiss/uim@latest add stepper

The CLI also adds what it builds on: Icon.

Manual

  1. Install the dependencies:
npx expo install cn phosphor-react-native react-native-svg
  1. Add the pieces it builds on: Icon, and the primitives components/primitives/number-field.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/interactions/stepper.tsx:

uniwind/components/interactions/stepper.tsx
import { cn } from "cn"
import { MinusIcon, PlusIcon } from "phosphor-react-native"
import type * as React from "react"
import { View, type ViewProps } from "react-native"
import * as NumberFieldPrimitive from "@/components/primitives/number-field"
import { resolveClassName } from "@/components/primitives/use-render"
import { Icon } from "@/components/ui/icon"

function Stepper({
  value,
  defaultValue = 0,
  onValueChange,
  className,
  ...props
}: Omit<
  React.ComponentProps<typeof NumberFieldPrimitive.Root>,
  1. Update the import paths to match your project.

Usage

import { Stepper, StepperInput, StepperGroup, StepperDecrement, StepperIncrement, StepperSeparator } from "@/components/interactions/stepper"
<Stepper defaultValue={1} min={0} max={10}>
  <StepperInput accessibilityLabel="Quantity" />
  <StepperGroup>
    <StepperDecrement />
    <StepperSeparator />
    <StepperIncrement />
  </StepperGroup>
</Stepper>

Compose StepperInput and a StepperGroup containing StepperDecrement, StepperSeparator and StepperIncrement inside Stepper.

Use it uncontrolled with defaultValue (0 by default) or controlled with value and onValueChange. Clearing the field never reports null; the change is cancelled instead. min, max, step, format (Intl.NumberFormatOptions), locale and disabled are passed to the number field.

Examples

Print

A print settings list with copies, a percentage scale stepping by 5, and a disabled row.

iOS
Stepper (print) on iOS in light mode

API Reference

Stepper

The root that holds the value and limits.

ExtendsReact.ComponentProps<typeof NumberFieldPrimitive.Root>
PropType
About
The current value when controlled.
Type
number
Default
—
About
The initial value when uncontrolled. Defaults to 0.
Type
number
Default
0
About
Called with the new number and change details.
Type
( value: number, eventDetails: NumberFieldPrimitive.ChangeEventDetails ) => void
Default
—

StepperInput

The editable, right-aligned value.

ExtendsReact.ComponentProps<typeof NumberFieldPrimitive.Input>

StepperGroup

The rounded field that holds the buttons.

ExtendsReact.ComponentProps<typeof NumberFieldPrimitive.Group>

StepperDecrement

The minus button.

ExtendsReact.ComponentProps<typeof NumberFieldPrimitive.Decrement>

StepperIncrement

The plus button.

ExtendsReact.ComponentProps<typeof NumberFieldPrimitive.Increment>

StepperSeparator

A thin divider between the buttons.

ExtendsViewProps

Accessibility

  • The minus and plus buttons have the button role.
  • Label the value with accessibilityLabel or accessibilityLabelledBy on StepperInput.
  • The separator is hidden from screen readers.

Notes

  • A selection haptic plays on each step, if the app provides an adapter through HapticsProvider.
  • The group uses bg-control. Buttons use bg-surface-raised with a small shadow (bg-label-quaternary in dark mode) and bg-control-pressed when pressed; a disabled button or one at its limit uses bg-control-disabled with a text-label-quaternary icon.
  • Disabled text uses text-label-quaternary.