DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

Stepper

Tap-only numeric quantity control — five variants (soft, outline, ghost, split, joined), single-tab-stop spinbutton keyboard model, hold-to-repeat, trash-at-min removal, value formatting, on-color tone, and native form participation.

A Stepper changes a number a few taps at a time: cart quantities, guest counts, portions, goals. It is not a wizard step indicator (that component is the future Steps; some systems, Astryx included, use the name "Stepper" for wizards — Tessera does not), and it is not a typing surface: when people should enter a number, reach for NumberField.

Playground

Installation

pnpm add @tessinaui/ui

Usage

import { Stepper } from "@tessinaui/ui";
{/* Basic uncontrolled */}
<Stepper defaultValue={1} min={0} max={10} />

{/* Controlled, with change reasons */}
<Stepper
  value={quantity}
  onValueChange={(next, { reason }) => setQuantity(next)}
  onValueCommitted={(settled) => saveQuantity(settled)}
  min={1}
  max={99}
/>

{/* Cart row: trash at minimum */}
<Stepper min={1} removable onRemove={removeLineItem} />

{/* Formatted value (visible text AND aria-valuetext) */}
<Stepper formatValue={(v) => `${v} kg`} step={5} largeStep={25} />

When not to use

  • Typed entry, large jumps, precise wide ranges → NumberField.
  • Approximate values on a continuum → Slider.
  • Long fixed option lists (apparel sizes, case-packs) → Select.
  • Durations and times → TimePicker (the market default is a wheel, not a stepper).
  • Workout/measurement logging → a keypad-friendly NumberField; steppers are too slow for three-digit entry.

Examples

Default

A bounded quantity. The exhausted button dims in place — it never disappears, never collapses layout, and stays discoverable to assistive tech via aria-disabled.

Variants

soft shares one filled container, outline one bordered container, ghost is bare glyphs, split is the travel-app pattern of separate outlined circles, and joined is the iOS-style fused two-segment control (its value always lives outside — see the external-value example). emphasizeIncrement fills only the + button, encoding "adding is the primary action".

Sizes

Five sizes on the input-family ladder — 32 / 36 / 40 / 48 / 56px, the same heights Field and Select render, so a Stepper and a Select with the same size line up in one form row. Every button floors to 44×44px on phone viewports.

The button ladder is a different scale (32 / 40 / 48 / 56 / 96), so a footer row pairs the two one token apart — Stepper size="lg" with Button size="md" are both 48px, xs with xs are both 32px.

Intents

The full palette on both the soft container and split buttons. intent="error" also stamps aria-invalid.

States

disabled (out of the tab order), readOnly (focusable-but-frozen — the value keeps full contrast), pending (an async mutation is in flight: aria-busy, stepping refused), the at-minimum boundary, and the skeleton.

With label and validation

label names the control; supportingText is describedby-wired so assistive tech hears it. Swap the helper and intent together for validation.

Cart row (removable)

At min with removable, the decrement becomes a trash action with a Remove … accessible name and fires onRemove instead of stepping — the dominant post-add cart pattern.

Guest counter panel

Label + sublabel rows with split steppers end-aligned, inside a Popover — the composition the Select docs route guest/traveler pickers to. Limits are communicated as helper text near the panel, not on the control.

Add → stepper

An Add button and the stepper share one geometry, so the control morphs in place after the first add and collapses back at zero via removable.

Sheet footer with CTA

Height-matched beside the primary action, quantity driving the CTA's live price.

Formatted values

formatValue renders units, zero-padding, or locale separators — and feeds aria-valuetext, so the announcement matches the pixels. largeStep covers PageUp/PageDown and Shift+Arrow.

External value

joined never shows its value inline (pair it with an adjacent readout); showValue={false} builds hero displays with the number rendered at display scale. In both, aria-valuenow/aria-valuetext keep the control fully announced.

On color

On a colored Surface the tone is inherited; every ink derives from the surface's currentColor contract. intent="primary" falls back to the surface ink — a self-colored chip cannot adapt to an arbitrary plate.

In a form

name renders a hidden input, so steppers submit natively.

Keyboard

One Tab stop: the control itself. The −/+ buttons are tabindex="-1" — still reachable for touch and voice AT, redundant for keyboard.

KeyAction
TabFocus the stepper
ArrowUp / ArrowDown± step
Shift + Arrow± largeStep (default 10 × step)
PageUp / PageDown± largeStep
Home / EndJump to min / max (bounded sides only)

Press-and-hold on the buttons auto-repeats (400ms mouse / 600ms touch delay, then every 60ms); moving the pointer ≥8px cancels the press, and a touch tap steps on release so scrolls that start on a button never change the value.

API Reference

Props

PropTypeDefaultDescription
variant"soft" | "outline" | "ghost" | "split" | "joined""soft"Visual variant. pill/floating remain as deprecated aliases of soft/split for one minor
size"xs" | "sm" | "md" | "lg" | "xl""md"Density — 32/36/40/48/56px control heights (the input-family ladder, shared with Field/Select); 44px phone floors
intent"none" | "primary" | "error" | "warning" | "success" | "info""none"Color intent; error also sets aria-invalid
tone"default" | "on-color"inheritedOn-color adapts every ink to a colored plate; inherited from a surrounding Surface
rounded"none" | "sm" | "md" | "lg" | "full""full"Radius cascade — container and buttons follow proportionally (square split buttons at sm/md)
value / defaultValuenumber— / 0Controlled / uncontrolled value
min / maxnumber-Infinity / InfinityBounds. Steps never exit the range; a value starting outside steps toward it
stepnumber1Per-press change (must be > 0)
largeStepnumber10 × stepApplied by PageUp/PageDown and Shift+Arrow
disabledbooleanfalseRemoves the control from the tab order
readOnlybooleanfalseFocusable-but-frozen; announces aria-readonly, value keeps full contrast
pendingbooleanfalseAsync in flight: aria-busy, stepping refused, value pulses
holdToRepeatbooleantruePress-and-hold auto-repeat
removablebooleanfalseAt value <= min the decrement becomes a remove action
onRemove() => void—Fired by the remove action
emphasizeIncrementbooleanfalseFills the + button with the intent color
showValuebooleantrueHide the inline readout (external/hero values). joined never shows one
formatValue(value: number) => string—Formats the readout AND aria-valuetext
onValueChange(value, { reason }) => void—Per step; reason ∈ increment-press · decrement-press · hold · keyboard
onValueCommitted(value: number) => void—Once per settled interaction (pointer release, key release)
onChange(value: number) => void—Deprecated — use onValueChange
label / supportingTextstring—Visible label above / describedby-wired helper below
groupLabelstring"Quantity"Accessible name when no label — set it for anything that isn't one
incrementLabel / decrementLabel / removeLabelstringcomposedButton names; default "Increase/Decrease/Remove {label}"
incrementIcon / decrementIcon / removeIconReactNode+/−/trashCustom glyphs
dir"ltr" | "rtl"—Stamped only when set; buttons mirror, Up/Down never flip
name / formstring—Native form participation via a hidden input
classNamestring—Merged onto the outermost element

Arbitrary aria-* props land on the spinbutton control; every other rest prop (data-*, id, style, handlers) lands on the outermost root (§7 split).

Accessibility model

The control is a single-tab-stop role="spinbutton" container (the APG Quantity / Date Picker Spin Buttons shape): it carries aria-valuenow/min/max/text and the full key set, while the buttons sit at tabindex="-1" with composed names and aria-controls. Boundary, readOnly and pending states expose the buttons as aria-disabled — never the native attribute, which would drop focus to <body> when a boundary is reached under the cursor. Pointer steps are mirrored into a polite live region (cleared after 2s); keyboard steps announce through the spinbutton itself. Negative values announce with a real minus (U+2212).

StepperSkeleton

variant / size / rounded / showLabel / valueChars — the silhouette follows the real control's maps (§2b): bars are line-boxes of the value/label type ladder, and valueChars reserves the width of the widest expected value.

Notes

  • Never leave a stepper valueless — always provide defaultValue (cart-style contexts usually want 1).
  • The value never truncates; it reserves width from the widest bound and uses tabular-nums so buttons don't shift as digits change.
  • data-slot attributes (stepper, stepper-decrement, stepper-value, stepper-increment, stepper-status, stepper-label, stepper-supporting-text, stepper-root) plus data-at-min/at-max/disabled/readonly/pending/variant/size make every part and state addressable by tests and agents.
StatusSurface

On this page

PlaygroundInstallationUsageWhen not to useExamplesDefaultVariantsSizesIntentsStatesWith label and validationCart row (removable)Guest counter panelAdd → stepperSheet footer with CTAFormatted valuesExternal valueOn colorIn a formKeyboardAPI ReferencePropsAccessibility modelStepperSkeletonNotes