Skip to main content

Stepper

Stepper is a navigation aid that shows progress through a multistep flow, such as checkout, onboarding, or account setup.

Import

import { Stepper } from '@vortexlabs/vortex';
import type { StepperProps } from '@vortexlabs/vortex';

Other links

  1. ProfileName and avatar
  2. WorkspaceInvite your team
  3. ReviewConfirm details
<Stepper
  ariaLabel="Setup progress"
  defaultActiveIndex={1}
  size="sm"
  variant="inline"
>
  <Stepper.Item label="Account" description="Email and password" />
<Stepper.Item label="Profile" description="Name and avatar" />
<Stepper.Item label="Workspace" description="Invite your team" />
<Stepper.Item label="Review" description="Confirm details" />
</Stepper>

Features

  • Shows progress through a flow as numbered steps.
  • Marks steps as completed, current, upcoming, or error.
  • Lets people go back to completed steps.
  • Supports horizontal, stacked, and vertical layouts in two sizes.
  • Shows step content under the active step in vertical layouts.
  • Supports controlled and uncontrolled active steps.

Anatomy

<Stepper ariaLabel="Setup progress">
  <Stepper.Item label="Account" />
  <Stepper.Item label="Profile" />
</Stepper>
Component Part
Description
Tracks the active step and derives each step's status from its position.
Shows one step's indicator, label, and description.

Sizes

Stepper has two sizes: sm and md. The default is sm. Size scales the indicators, labels, and descriptions.

  1. ProfileName and avatar
  2. ReviewConfirm details
  1. ProfileName and avatar
  2. ReviewConfirm details
import { Stack, Stepper } from "@vortexlabs/vortex";

<Stack gap={8}>
  <Stepper ariaLabel="Small stepper" defaultActiveIndex={1} size="sm">
    <Stepper.Item label="Account" description="Email and password" />
    <Stepper.Item label="Profile" description="Name and avatar" />
    <Stepper.Item label="Review" description="Confirm details" />
  </Stepper>
  <Stepper ariaLabel="Medium stepper" defaultActiveIndex={1} size="md">
    <Stepper.Item label="Account" description="Email and password" />
    <Stepper.Item label="Profile" description="Name and avatar" />
    <Stepper.Item label="Review" description="Confirm details" />
  </Stepper>
</Stack>

Variants

inline places the step text beside the indicator; stacked places it underneath. The default is inline.

  1. ProfileName and avatar
  2. ReviewConfirm details
  1. ProfileName and avatar
  2. ReviewConfirm details
<Stack gap={8}>
  <Stepper ariaLabel="Inline stepper" defaultActiveIndex={1} variant="inline">
    <Stepper.Item label="Account" description="Email and password" />
    <Stepper.Item label="Profile" description="Name and avatar" />
    <Stepper.Item label="Review" description="Confirm details" />
  </Stepper>
  <Stepper ariaLabel="Stacked stepper" defaultActiveIndex={1} variant="stacked">
    <Stepper.Item label="Account" description="Email and password" />
    <Stepper.Item label="Profile" description="Name and avatar" />
    <Stepper.Item label="Review" description="Confirm details" />
  </Stepper>
</Stack>

Vertical Orientation

Set orientation="vertical" to run steps top to bottom. A step's children show only while it is active or has an error. Vertical steppers ignore variant.

  1. Install the packageAdd Vortex to your project
    Run bun add @vortexlabs/vortex in your project root.
  2. Add the providerWrap your app
  3. Import the stylesLoad the theme CSS
  4. Build somethingUse your first component
"use client";

import { Button, Stack, Stepper, Text } from "@vortexlabs/vortex";
import React from "react";

const steps = [
  { label: "Install the package", description: "Add Vortex to your project", content: "Run bun add @vortexlabs/vortex in your project root." },
  { label: "Add the provider", description: "Wrap your app", content: "Render VortexProvider at the root." },
  { label: "Import the styles", description: "Load the theme CSS", content: "Import your theme's globals.css once." },
  { label: "Build something", description: "Use your first component", content: "Try rendering a Button." },
];

export const StepperVerticalExample = () => {
  const [activeIndex, setActiveIndex] = React.useState(0);
  const contentRefs = React.useRef<(HTMLDivElement | null)[]>([]);
  const focusContentRef = React.useRef(false);

  // Next collapses with its step, so move focus to the new step's content
  React.useEffect(() => {
    if (!focusContentRef.current) return;
    focusContentRef.current = false;
    contentRefs.current[activeIndex]?.focus();
  }, [activeIndex]);

  const handleNext = (index: number) => {
    focusContentRef.current = true;
    setActiveIndex(index + 1);
  };

  return (
    <Stepper
      ariaLabel="Onboarding progress"
      orientation="vertical"
      activeIndex={activeIndex}
      onChange={({ index }) => setActiveIndex(index)}
    >
      {steps.map((step, index) => (
        <Stepper.Item key={step.label} label={step.label} description={step.description}>
          <div
            ref={(element) => {
              contentRefs.current[index] = element;
            }}
            tabIndex={-1}
            style={{ outline: "none" }}
          >
            <Stack gap={3} align="start">
              <Text size="body-sm-desktop">{step.content}</Text>
              {index < steps.length - 1 && (
                <Button size="sm" onClick={() => handleNext(index)}>
                  Next
                </Button>
              )}
            </Stack>
          </div>
        </Stepper.Item>
      ))}
    </Stepper>
  );
};

Hidden Text

Set textHidden to show only the indicators. The labels stay available to assistive technology.

  1. Profile
  2. Workspace
  3. Review
<Stepper ariaLabel="Setup progress" defaultActiveIndex={1} textHidden>
  <Stepper.Item label="Account" />
  <Stepper.Item label="Profile" />
  <Stepper.Item label="Workspace" />
  <Stepper.Item label="Review" />
</Stepper>

Step Status

Steps before the active step are completed; later steps are upcoming. Set status on Stepper.Item to mark a step as error, or as completed when people already finished a later step.

  1. WorkspaceInvite your team
  2. ReviewConfirm details
  1. AccountEmail and password
  2. ReviewConfirm details
<Stack gap={8}>
  <Stepper ariaLabel="Setup progress" activeIndex={2}>
    <Stepper.Item label="Account" description="Email and password" />
    <Stepper.Item label="Profile" description="Name is missing" status="error" />
    <Stepper.Item label="Workspace" description="Invite your team" />
    <Stepper.Item label="Review" description="Confirm details" />
  </Stepper>
  <Stepper ariaLabel="Edit progress" activeIndex={0}>
    <Stepper.Item label="Account" description="Email and password" />
    <Stepper.Item label="Profile" description="Name and avatar" status="completed" />
    <Stepper.Item label="Workspace" description="Invite your team" status="completed" />
    <Stepper.Item label="Review" description="Confirm details" />
  </Stepper>
</Stack>

Clickable Steps

Completed and error steps are clickable by default, so people can go back. Set clickable to allow skipping ahead, or clickable={false} to lock a step. The current step is never clickable.

  1. Account
  1. AccountLocked
  2. Workspace
  3. Review
<Stack gap={8}>
  <Stepper ariaLabel="Open navigation" defaultActiveIndex={0}>
    <Stepper.Item label="Account" />
    <Stepper.Item label="Profile" clickable />
    <Stepper.Item label="Workspace" clickable />
    <Stepper.Item label="Review" clickable />
  </Stepper>
  <Stepper ariaLabel="Locked progress" defaultActiveIndex={2}>
    <Stepper.Item label="Account" description="Locked" clickable={false} />
    <Stepper.Item label="Profile" />
    <Stepper.Item label="Workspace" />
    <Stepper.Item label="Review" />
  </Stepper>
</Stack>

Controlled Selection

Set activeIndex and update it in onChange to drive the steps from your flow. Setting it past the last step marks every step as completed.

  1. Cart
  2. Shipping
  3. Payment
  4. Review
Step 1: Cart
"use client";

import { Button, Stack, Stepper, Text } from "@vortexlabs/vortex";
import React from "react";

const steps = ["Cart", "Shipping", "Payment", "Review"];

export const StepperControlledExample = () => {
  const [activeIndex, setActiveIndex] = React.useState(0);
  const placed = activeIndex === steps.length;

  return (
    <Stack gap={6} width="100%">
      <Stepper
        ariaLabel="Checkout progress"
        activeIndex={activeIndex}
        onChange={({ index }) => setActiveIndex(index)}
      >
        {steps.map((label) => (
          <Stepper.Item key={label} label={label} />
        ))}
      </Stepper>
      <Text size="body-md-desktop">
        {placed ? "Order placed. Thank you!" : `Step ${activeIndex + 1}: ${steps[activeIndex]}`}
      </Text>
      <Stack direction="row" gap={2}>
        <Button
          variant="secondary"
          disabled={activeIndex === 0 || placed}
          onClick={() => setActiveIndex((index) => index - 1)}
        >
          Back
        </Button>
        <Button disabled={placed} onClick={() => setActiveIndex((index) => index + 1)}>
          {activeIndex === steps.length - 1 ? "Place order" : "Continue"}
        </Button>
      </Stack>
    </Stack>
  );
};

Use ProgressBar for progress without named steps, and Tabs for views people can switch between in any order.

Accessibility

Key
Description
TabShiftTab
Move focus to the next or previous clickable step.
EnterSpace
Go to the focused step.
Status icons aren't announced; put status people must hear in description.
Provide ariaLabel to name the step list.
When an action inside a vertical step advances the flow, move focus to the next step's content.