Tessera UI

Stepper

Numeric quantity control with increment and decrement buttons. Two layout variants (pill and floating), five sizes, full intent palette, min/max/step, disabled/readOnly states, and LTR/RTL support.

Installation

pnpm add @tessinaui/ui

Usage

import { Stepper } from "@tessinaui/ui";
{/* Basic uncontrolled */}
<Stepper />

{/* Controlled */}
<Stepper value={quantity} onChange={setQuantity} min={1} max={99} />

{/* Floating variant */}
<Stepper variant="floating" />

{/* With label + validation */}
<Stepper
  label="Quantity"
  intent="error"
  supportingText="Maximum quantity reached"
  value={10}
  min={1}
  max={10}
/>

{/* Custom step */}
<Stepper step={5} defaultValue={0} />

{/* RTL */}
<Stepper dir="rtl" label="الكمية" />

Playground

Preview

Showcase

Preview

API Reference

Props

PropTypeDefaultDescription
variant"pill" | "floating""pill"pill — decrement, value and increment share one rounded-full container; floating — each button is a separate filled circle
size"xs" | "sm" | "md" | "lg" | "xl""md"Size token
intent"none" | "primary" | "error" | "warning" | "success" | "info""none"Color intent — affects button/pill background and text color
valuenumberControlled value
defaultValuenumber0Uncontrolled initial value
minnumber-InfinityMinimum value — decrement button is disabled at this value
maxnumberInfinityMaximum value — increment button is disabled at this value
stepnumber1Amount to change per click
disabledbooleanfalseDisable all interaction; container gets opacity-60
readOnlybooleanfalseButtons are non-interactive (opacity-40, pointer-events-none) — value is still visible
onChange(value: number) => voidCalled with the new value after each change
labelstringVisible label rendered above the stepper
supportingTextstringHelper / validation text below the stepper (colored by intent)
groupLabelstring"Quantity"aria-label for the stepper group — used when no visible label is provided
decrementLabelstring"Decrease"aria-label for the − button
incrementLabelstring"Increase"aria-label for the + button
dir"ltr" | "rtl""ltr"Text direction. In RTL the + button is on the leading (right) side and − is on the trailing (left) side
rounded"none" | "sm" | "md" | "lg" | "full""full"Border radius of the pill container and pill buttons. Floating buttons always stay rounded-full.
classNamestringAdditional class on the root wrapper

Variants

VariantDescription
pillAll three elements (−, value, +) are inside one rounded-full container. Buttons are transparent; the pill provides the background.
floatingEach button is a standalone filled circle. The value floats between them without a container background.

Intents

IntentPill bgFloating button bgText
nonebg-secondarybg-secondarytext-foreground
primarybg-primarybg-primarytext-on-primary
errorbg-error-lightbg-error-lighttext-error
warningbg-warning-lightbg-warning-lighttext-warning
successbg-success-lightbg-success-lighttext-success
infobg-info-lightbg-info-lighttext-info

Notes

  • Controlled vs uncontrolled: When value is provided, the component is fully controlled. Use defaultValue for uncontrolled usage.
  • Min/Max clamping: Values are always clamped between min and max. The respective button dims to opacity-30 and becomes non-interactive at the boundary.
  • RTL: dir="rtl" swaps the + and − button sides. The + button moves to the inline-start (right) side, matching standard RTL number-input conventions (e.g. Carbon, Fluent).
  • Accessibility: The control renders as role="group" with aria-label or aria-labelledby. The value uses <output aria-live="polite"> so screen readers announce changes.
  • readOnly vs disabled: disabled prevents all interaction and dims the whole control; readOnly dims only the buttons (the value remains clearly readable).

On this page