Primitives

Type-safe primitive components for building stepper UIs

The Stepper object returned by defineStepper provides a set of primitive components that are type-safe and bound to your step definitions. They handle ARIA attributes, keyboard navigation, and data attributes automatically; stepper state is provided by Scoped or Stepper.Root.

Overview

import * as React from "react"
import {  } from "@stepperize/react"

const {  } = (
  { : "shipping", : "Shipping" },
  { : "payment", : "Payment" },
  { : "confirmation", : "Confirmation" }
);

function () {
  return (
    <.>
      {({  }) => (
        <>
          <.>
            {...(() => (
              <. ={.} ={.}>
                <.>
                  <. />
                  <.>{.}</.>
                </.>
                <. />
              </.>
            ))}
          </.>

          <. ="shipping">
            Shipping form here
          </.>
          <. ="payment">
            Payment form here
          </.>
          <. ="confirmation">
            Confirmation here
          </.>

          <.>
            <.>Previous</.>
            <.>Next</.>
          </.>
        </>
      )}
    </.>
  );
}

Component Hierarchy

Stepper.Root
├── Stepper.List
│   └── Stepper.Item (for each step)
│       ├── Stepper.Trigger
│       │   ├── Stepper.Indicator
│       │   ├── Stepper.Title
│       │   └── Stepper.Description
│       └── Stepper.Separator
├── Stepper.Content (for each step)
└── Stepper.Actions
    ├── Stepper.Prev
    └── Stepper.Next

Accessibility

The primitives implement the WAI-ARIA Tabs pattern so the stepper is announced and operable with keyboard and assistive technologies. The step list is exposed as a tablist, each step trigger as a tab, and each step content as a tabpanel. Stepper semantics are added with aria-current="step" on the active tab.

Roles and attributes by component

ComponentRole / ARIADescription
Stepper.Rootrole="group", aria-label="Stepper"Groups the widget. You can override it by passing aria-label on Stepper.Root (e.g. aria-label="Checkout steps").
Stepper.Listrole="tablist", aria-orientationContainer for the tabs; orientation is horizontal or vertical from the orientation prop.
Stepper.Triggerrole="tab", id="step-{stepId}", aria-controls="step-panel-{stepId}", aria-current="step" (when active), aria-selected (true/false), aria-posinset, aria-setsizeEach trigger is a tab; roving tabindex (only the active tab has tabIndex={0}).
Stepper.Contentrole="tabpanel", id="step-panel-{stepId}", aria-labelledby="step-{stepId}", tabIndex={0}Panel for the current step; tabIndex={0} keeps it in the tab order when it has no focusable children.
Stepper.Indicatoraria-hidden="true"Treated as decorative; the trigger’s accessible name comes from the title/content.
Stepper.Separatoraria-hidden="true", tabIndex={-1}Decorative; not in tab order and hidden from assistive tech.
Stepper.Prev / Stepper.Nextdisabled and aria-disabled when at first/last stepButtons are disabled and announced as disabled.

Keyboard interaction

When focus is on a tab (trigger) inside the list:

KeyAction
ArrowRight / ArrowDownMove focus and activation to the next step. Focus does not wrap: on the last step, ArrowRight/ArrowDown has no effect.
ArrowLeft / ArrowUpMove focus and activation to the previous step. On the first step, ArrowLeft/ArrowUp has no effect.
HomeMove focus and activation to the first step.
EndMove focus and activation to the last step.
TabMove focus out of the tablist (e.g. to the active tabpanel or the next focusable element).

The tabpanel (content) has tabIndex={0} so that when it has no focusable children, users can still Tab into it and then Tab out to the next region.

References


Accessing step data

You can get step data in three ways when using primitives:

Render prop on Stepper.Root

The root accepts a function {({ stepper }) => (...)} that receives the full stepper instance. Use it when you need current step, navigation, flow, lookup, and metadata in the same place you render the list and content:

<Stepper.Root>
  {({ stepper }) => (
    <>
      <div>Current: {stepper.state.current.data.title}</div>
      <Stepper.List>...</Stepper.List>
      <Stepper.Content step="shipping">...</Stepper.Content>
      <Stepper.Actions>
        <Stepper.Prev />
        <Stepper.Next>{stepper.state.isLast ? "Finish" : "Next"}</Stepper.Next>
      </Stepper.Actions>
    </>
  )}
</Stepper.Root>

useStepper() — global stepper in any child

Any component inside Stepper.Root can call useStepper() (from your defineStepper result) to access the full stepper: current step, next(), prev(), goTo(), when(), switch(), etc. Use it for content panels, navigation logic, or any component that needs to know "where we are" and control the flow.

const { Stepper, useStepper } = defineStepper(/* step1, step2, ... */);

function ShippingForm() {
  const stepper = useStepper();
  return (
    <Stepper.Content step="shipping">
      <p>Step: {stepper.state.current.data.title}</p>
      <button onClick={() => stepper.navigation.next()}>Continue</button>
    </Stepper.Content>
  );
}

useStepItemContext() — this item's step

useStepItemContext() returns the step data, index, status, and metadata for the Stepper.Item that wraps the component that calls the hook. Use it when you need to read or style based on "this step" (e.g. this item's status or index) without passing the step id as a prop.

Import: useStepItemContext is exported from @stepperize/react/primitives (not from the main @stepperize/react entry).

import { useStepItemContext } from "@stepperize/react/primitives";

Same type as useStepper().state.current, different value:

  • useStepper().state.current — The active step (the one the user is on). One value for the whole stepper; it changes when you navigate.
  • useStepItemContext()This item's step. Each Stepper.Item has its own step; the hook returns that step's data and its computed status (active/success/inactive). For example, inside <Stepper.Item step="payment"> you get payment's data and payment's status (e.g. "inactive" if the user is still on shipping).

When to use: Inside custom trigger wrappers, indicators, titles, or any component rendered inside Stepper.Item that needs this step's data, index, status, or metadata without receiving the step id from the parent.

When not to use: For global stepper state (which step is active, navigation, flow) use useStepper(). For content panels use Stepper.Content or stepper.flow.switch(); those live outside items.

Requirement: The hook must be called from a component that is a descendant of Stepper.Item. Otherwise it throws. For components outside items, use useStepper() instead.

Return value

PropertyTypeDescription
dataStepThe step object ({ id, ... }) for this item
indexnumberZero-based index of this step in the stepper
status"active" | "success" | "inactive""active" = current step, "success" = past step, "inactive" = future step
metadata{ get, set, reset }Get/set/reset metadata for this step (same API as state.current.metadata)

Example: custom indicator with step number

Use item.index to show a numeric indicator (1, 2, 3) without passing the step id:

import * as React from "react";
import {  } from "@stepperize/react";
import {  } from "@stepperize/react/primitives";

const {  } = (
  { : "shipping", : "Shipping" },
  { : "payment", : "Payment" },
  { : "review", : "Review" }
);

function () {
  const  = ();
  return <>{. + 1}</>;
}

function () {
  return (
    <.>
      {({  }) => (
        <.>
          {...(() => (
            <. ={.} ={.}>
              <.>
                <.>
                  < />
                </.>
                <.>{.}</.>
              </.>
              <. />
            </.>
          ))}
        </.>
      )}
    </.>
  );
}

Example: style trigger by status

Use item.status to change the trigger's appearance (e.g. active vs inactive) without comparing step ids yourself:

import * as React from "react";
import {  } from "@stepperize/react";
import {  } from "@stepperize/react/primitives";

const {  } = (
  { : "shipping", : "Shipping" },
  { : "payment", : "Payment" }
);

function ({  }: { : React. }) {
  const  = ();
  const  = . === "active";

  return (
    <.
      ={() => (
        <
          ="button"
          {...}
          ={ ? "border-blue-500 bg-blue-50" : "border-gray-300 bg-white"}
        >
          {}
        </>
      )}
    >
      {}
    </.>
  );
}

// Use inside Item
// <Stepper.Item step="shipping">
//   <CustomTrigger>
//     <Stepper.Indicator />
//     <Stepper.Title>Shipping</Stepper.Title>
//   </CustomTrigger>
// </Stepper.Item>

Example: show icon only when step is completed

Use item.status === "success" to display a checkmark or icon for past steps:

const item = useStepItemContext();
const isCompleted = item.status === "success";

return (
  <Stepper.Trigger render={(props) => (
    <button {...props}>
      {isCompleted ? <CheckIcon /> : <Stepper.Indicator />}
      <Stepper.Title>{item.data.title}</Stepper.Title>
    </button>
  )}>
    ...
  </Stepper.Trigger>
);

Summary

NeedUse
This item's step data, index, status, or metadatauseStepItemContext() (inside Stepper.Item)
Which step is active; navigation; flowuseStepper()
Current step in the same place you render list/contentRender prop on Stepper.Root: {({ stepper }) => ...}

Stepper.Root

Container component that provides context for all primitives. Internally uses Scoped to manage stepper state.

<Stepper.Root orientation="horizontal">
  {({ stepper }) => (
    <div>Current: {stepper.state.current.data.title}</div>
  )}
</Stepper.Root>

Props

PropTypeDefaultDescription
orientation"horizontal" | "vertical"Layout orientation; used for data-orientation and ARIA
initialStepGet.Id<Steps>Initial active step (passed to Scoped)
initialMetadataPartial<Record<Get.Id<Steps>, Metadata>>Initial metadata (passed to Scoped)
childrenReactNode | (({ stepper }) => ReactNode)Content or render function

Data Attributes

AttributeValue
data-component"stepper"
data-orientationValue of orientation prop (if passed)

Stepper.List

Ordered list container for step items. Renders as <ol> by default.

<Stepper.List>
  {stepper.state.all.map((step) => (
    <Stepper.Item key={step.id} step={step.id}>
      {/* ... */}
    </Stepper.Item>
  ))}
</Stepper.List>

Props

PropTypeDefaultDescription
render(props) => ReactElementundefinedCustom render function
childrenReactNodeTypically one or more Stepper.Item

Data Attributes

AttributeValue
data-component"stepper-list"
data-orientation"horizontal" or "vertical"

Accessibility

  • role="tablist", aria-orientation from orientation prop
  • Keyboard: Arrow keys (prev/next step), Home (first), End (last) — see Accessibility (ARIA)

Stepper.Item

Wrapper for a single step. Provides context for nested primitives.

<Stepper.Item step="shipping">
  <Stepper.Trigger>
    <Stepper.Indicator />
    <Stepper.Title>Shipping</Stepper.Title>
  </Stepper.Trigger>
  <Stepper.Separator />
</Stepper.Item>

Props

PropTypeDefaultDescription
stepStepIdRequiredThe step ID this item represents
render(props) => ReactElementundefinedCustom render function
childrenReactNodeTrigger, separator, etc.

Data Attributes

AttributeValue
data-component"stepper-item"
data-status"active", "success", or "inactive"

Stepper.Trigger

Clickable button to navigate to a step. Must be used within Stepper.Item.

<Stepper.Item step="step-1">
  <Stepper.Trigger onClick={() => console.log("Custom click")}>
    <Stepper.Indicator />
    <Stepper.Title>Step 1</Stepper.Title>
  </Stepper.Trigger>
</Stepper.Item>

Props

PropTypeDefaultDescription
onClickMouseEventHandlerundefinedCalled after the default navigation to the step; use for analytics or side effects. The step still changes.
disabledbooleanfalseWhen true, the trigger does not navigate and is not focusable (tabIndex=-1).
render(props) => ReactElementundefinedCustom render function
childrenReactNodeIndicator, title, etc.

Data Attributes

Same as Stepper.Item.

Accessibility

  • role="tab", id="step-{stepId}", aria-controls, aria-current="step" (when active), aria-selected, aria-posinset, aria-setsize
  • Roving tabindex: only the active tab has tabIndex={0}; inactive tabs have tabIndex={-1}
  • Keyboard: Arrow keys, Home, End — handled by Stepper.List; see Accessibility (ARIA)

Stepper.Indicator

Visual indicator for the step (e.g. number or icon). Must be used within Stepper.Item. Renders whatever you pass as children or via render; there is no built-in default content.

{/* Custom: show step index via useStepItemContext or children */}
<Stepper.Indicator>{item.index + 1}</Stepper.Indicator>

{/* Custom: show icon */}
<Stepper.Indicator>
  <CheckIcon />
</Stepper.Indicator>

Props

PropTypeDefaultDescription
render(props) => ReactElementundefinedCustom render function
childrenReactNodeContent (e.g. number, icon)

Data Attributes

Same as Stepper.Item.

Accessibility

  • aria-hidden="true" (indicator is decorative, the title provides the accessible name)

Stepper.Title

Renders the step title. Can be used within or outside Stepper.Item.

<Stepper.Item step="shipping">
  <Stepper.Trigger>
    <Stepper.Title>Shipping Address</Stepper.Title>
  </Stepper.Trigger>
</Stepper.Item>

Props

PropTypeDefaultDescription
render(props) => ReactElementundefinedCustom render function
childrenReactNodeTitle text

Data Attributes

Same as Stepper.Item when used inside an Item.


Stepper.Description

Renders the step description. Can be used within or outside Stepper.Item.

<Stepper.Item step="shipping">
  <Stepper.Trigger>
    <Stepper.Title>Shipping</Stepper.Title>
    <Stepper.Description>Enter your shipping address</Stepper.Description>
  </Stepper.Trigger>
</Stepper.Item>

Props

PropTypeDefaultDescription
render(props) => ReactElementundefinedCustom render function
childrenReactNodeDescription text

Data Attributes

Same as Stepper.Item when used inside an Item.


Stepper.Separator

Visual separator line between steps. Renders between items; hide the last separator in your layout (e.g. by rendering it only when not the last item in the list) or style it via CSS if needed.

<Stepper.List>
  {stepper.state.all.map((step, index) => (
    <React.Fragment key={step.id}>
      <Stepper.Item step={step.id}>
        <Stepper.Trigger>...</Stepper.Trigger>
      </Stepper.Item>
      {index < stepper.state.all.length - 1 && <Stepper.Separator />}
    </React.Fragment>
  ))}
</Stepper.List>

Props

PropTypeDefaultDescription
orientation"horizontal" | "vertical"InheritedOverride separator orientation
render(props) => ReactElementundefinedCustom render function

Data Attributes

AttributeValue
data-status"active", "success", or "inactive" (when passed explicitly or inside Item)
data-orientation"horizontal" or "vertical"

Accessibility

  • aria-hidden="true", tabIndex={-1} — decorative; not in tab order and hidden from assistive tech

Stepper.Content

Panel that displays content for a specific step. Hidden when that step is not the current step.

<Stepper.Content step="shipping">
  <ShippingForm />
</Stepper.Content>

<Stepper.Content step="payment">
  <PaymentForm />
</Stepper.Content>

<Stepper.Content step="confirmation">
  <ConfirmationView />
</Stepper.Content>

Props

PropTypeDefaultDescription
stepStepIdRequiredThe step ID this content is for (shown only when current step matches)
render(props) => ReactElementundefinedCustom render function
childrenReactNodeContent to display

Data Attributes

AttributeValue
data-component"stepper-content"
id"step-panel-{step}" (for aria-controls from the trigger)

The component does not set data-status, data-step, data-index, or data-orientation by default; pass them via props if needed.

Accessibility

  • role="tabpanel", id="step-panel-{stepId}", aria-labelledby linked to the trigger, tabIndex={0} so the panel is in the tab order when it has no focusable children
  • Only the panel for the current step is rendered; inactive steps do not render content

Stepper.Actions

Container for navigation buttons. Provides semantic grouping.

<Stepper.Actions>
  <Stepper.Prev>Previous</Stepper.Prev>
  <Stepper.Next>Next</Stepper.Next>
</Stepper.Actions>

Props

PropTypeDefaultDescription
render(props) => ReactElementundefinedCustom render function
childrenReactNodeNavigation buttons

Data Attributes

AttributeValue
data-component"stepper-actions"

Accessibility

  • No default role or aria-label; add role="group" and aria-label via props if you want the actions region announced.

Stepper.Prev

Button to navigate to the previous step. Automatically disabled on first step.

{/* Default behavior */}
<Stepper.Prev>Previous</Stepper.Prev>

{/* Custom disabled behavior */}
<Stepper.Prev>Back</Stepper.Prev>

{/* Custom click handler */}
<Stepper.Prev onClick={() => {
  console.log("Going back");
  stepper.navigation.prev();
}}>
  Previous
</Stepper.Prev>

Props

PropTypeDefaultDescription
onClickMouseEventHandlerundefinedCalled after default prev()
disabledbooleanfalseManually disable (default: disabled when stepper.state.isFirst)
render(props) => ReactElementundefinedCustom render function
childrenReactNodeButton content

Data Attributes

AttributeValue
data-component"stepper-prev"
data-disabledPresent when disabled

Accessibility

  • disabled and aria-disabled when on the first step; provide visible label via children (e.g. "Previous", "Back")

Stepper.Next

Button to navigate to the next step. Automatically disabled on last step.

{/* Default behavior */}
<Stepper.Next>Next</Stepper.Next>

{/* Dynamic label */}
<Stepper.Next>
  {stepper.state.isLast ? "Finish" : "Next"}
</Stepper.Next>

{/* Custom click with validation */}
<Stepper.Next onClick={async () => {
  const isValid = await validateCurrentStep();
  if (isValid) {
    stepper.navigation.next();
  }
}}>
  Continue
</Stepper.Next>

Props

PropTypeDefaultDescription
onClickMouseEventHandlerundefinedCalled after default next()
disabledbooleanfalseManually disable (default: disabled when stepper.state.isLast)
render(props) => ReactElementundefinedCustom render function
childrenReactNodeButton content

Data Attributes

AttributeValue
data-component"stepper-next"
data-disabledPresent when disabled

Accessibility

  • disabled and aria-disabled when on the last step; provide visible label via children (e.g. "Next", "Finish")

Custom Rendering

All primitives support the render prop for full control over the rendered element:

<Stepper.Trigger
  render={(props) => (
    <button {...props} className="my-custom-trigger">
      Custom Trigger
    </button>
  )}
/>

The render function receives all props including data attributes, event handlers, and ARIA attributes. Spread them onto your element (e.g. <button {...props} />) so ARIA and keyboard behavior are preserved.


Styling with Data Attributes

Use data attributes for CSS styling. Step status is one of "active", "success", or "inactive":

/* Active step (current) */
[data-status="active"] {
  color: blue;
}

/* Completed step (past) */
[data-status="success"] {
  color: green;
}

/* Inactive step (future) */
[data-status="inactive"] {
  color: gray;
}

/* Disabled (e.g. Prev/Next when at first/last step) */
[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Horizontal layout */
[data-orientation="horizontal"] {
  display: flex;
  flex-direction: row;
}

/* Vertical layout */
[data-orientation="vertical"] {
  display: flex;
  flex-direction: column;
}

Complete Example

import * as React from "react"
import {  } from "@stepperize/react"

const { ,  } = (
  { : "account", : "Account", : "Create your account" },
  { : "profile", : "Profile", : "Set up your profile" },
  { : "complete", : "Complete", : "All done!" }
);

function () {
  return (
    <. ="horizontal">
      {({  }) => (
        < ="stepper-container">
          {/* Step list */}
          <. ="stepper-list">
            {...(() => (
              <.
                ={.}
                ={.}
                ="stepper-item"
              >
                <. ="stepper-trigger">
                  <. ="stepper-indicator" />
                  < ="stepper-text">
                    <. ="stepper-title">
                      {.}
                    </.>
                    <. ="stepper-description">
                      {.}
                    </.>
                  </>
                </.>
                <. ="stepper-separator" />
              </.>
            ))}
          </.>

          {/* Step content */}
          < ="stepper-content-container">
            <. ="account">
              <>Create Account</>
              <>Enter your email and password</>
            </.>

            <. ="profile">
              <>Set Up Profile</>
              <>Tell us about yourself</>
            </.>

            <. ="complete">
              <>All Done!</>
              <>Your account is ready</>
            </.>
          </>

          {/* Navigation */}
          <. ="stepper-actions">
            <. ="stepper-prev">
              Previous
            </.>
            <. ="stepper-next">
              {.. ? "Finish" : "Next"}
            </.>
          </.>
        </>
      )}
    </.>
  );
}
Edit on GitHub

Last updated on

On this page