Stepper

The Stepper component guides multi-step flows with compound parts, keyboard navigation, and semantic progress states.

Usage

Compose the flow with Stepper.Root, Stepper.List, Stepper.Item, Stepper.Indicator, Stepper.Title, Stepper.Description, and Stepper.Separator.

import React from 'react';
import { Stepper } from '@nathan3boss/ui/stepper';
import { ExampleFrame } from '@docs-support/example-frame';

const steps = [
  {
    description: 'Create your login credentials.',
    title: 'Account',
    value: 'account',
  },
  {
    description: 'Confirm your delivery address.',
    title: 'Shipping',
    value: 'shipping',
  },
  {
    description: 'Choose your payment method.',
    title: 'Payment',
    value: 'payment',
  },
  {
    description: 'Review and place the order.',
    title: 'Review',
    value: 'review',
  },
];

export default () => {
  return (
    <ExampleFrame maxWidth="880px" width="100%">
      <Stepper.Root ariaLabel="Checkout progress" defaultValue="shipping">
        <Stepper.List>
          {steps.map((step, index) => (
            <Stepper.Item key={step.value} value={step.value}>
              <Stepper.Indicator />
              <Stepper.Title>{step.title}</Stepper.Title>
              <Stepper.Description>{step.description}</Stepper.Description>
              {index < steps.length - 1 ? <Stepper.Separator /> : null}
            </Stepper.Item>
          ))}
        </Stepper.List>
      </Stepper.Root>
    </ExampleFrame>
  );
};

Vertical Layout

Use orientation="vertical" for onboarding flows, setup checklists, or sidebars where the content benefits from more reading space.

import React from 'react';
import { Stepper } from '@nathan3boss/ui/stepper';
import { ExampleFrame } from '@docs-support/example-frame';

export default () => {
  return (
    <ExampleFrame width="360px">
      <Stepper.Root
        ariaLabel="Workspace setup"
        defaultValue="members"
        orientation="vertical"
        variant="compact"
      >
        <Stepper.List>
          <Stepper.Item value="workspace">
            <Stepper.Indicator />
            <Stepper.Title>Workspace</Stepper.Title>
            <Stepper.Description>Name and personalize the space.</Stepper.Description>
            <Stepper.Separator />
          </Stepper.Item>
          <Stepper.Item value="members">
            <Stepper.Indicator />
            <Stepper.Title>Members</Stepper.Title>
            <Stepper.Description>Invite your team and set roles.</Stepper.Description>
            <Stepper.Separator />
          </Stepper.Item>
          <Stepper.Item value="review">
            <Stepper.Indicator />
            <Stepper.Title>Review</Stepper.Title>
            <Stepper.Description>Check the settings before launch.</Stepper.Description>
          </Stepper.Item>
        </Stepper.List>
      </Stepper.Root>
    </ExampleFrame>
  );
};

Controlled Flow

Use the controlled API when the active step depends on external form state, async validation, or navigation buttons.

import React, { useState } from 'react';
import { Button } from '@nathan3boss/ui/button';
import { Stepper } from '@nathan3boss/ui/stepper';
import { ExampleFrame } from '@docs-support/example-frame';

const steps = [
  {
    description: 'Create your login credentials.',
    title: 'Account',
    value: 'account',
  },
  {
    description: 'Confirm your delivery address.',
    title: 'Shipping',
    value: 'shipping',
  },
  {
    description: 'Choose your payment method.',
    title: 'Payment',
    value: 'payment',
  },
  {
    description: 'Review and place the order.',
    title: 'Review',
    value: 'review',
  },
];

export default () => {
  const [value, setValue] = useState('shipping');
  const currentIndex = steps.findIndex((step) => step.value === value);

  return (
    <ExampleFrame gap="16px" maxWidth="880px" width="100%">
      <Stepper.Root ariaLabel="Checkout progress" onValueChange={setValue} value={value}>
        <Stepper.List>
          {steps.map((step, index) => (
            <Stepper.Item key={step.value} value={step.value}>
              <Stepper.Indicator />
              <Stepper.Title>{step.title}</Stepper.Title>
              <Stepper.Description>{step.description}</Stepper.Description>
              {index < steps.length - 1 ? <Stepper.Separator /> : null}
            </Stepper.Item>
          ))}
        </Stepper.List>
      </Stepper.Root>

      <div style={{ display: 'flex', gap: '12px' }}>
        <Button
          disabled={currentIndex === 0}
          onClick={() => setValue(steps[Math.max(currentIndex - 1, 0)].value)}
          variant="outline"
        >
          Back
        </Button>
        <Button
          disabled={currentIndex === steps.length - 1}
          onClick={() => setValue(steps[Math.min(currentIndex + 1, steps.length - 1)].value)}
        >
          Next step
        </Button>
      </div>
    </ExampleFrame>
  );
};

States And Accessibility

Use explicit status values for exceptional cases like errors, while the current step and completed history are handled automatically.

import React from 'react';
import { Stepper } from '@nathan3boss/ui/stepper';
import { ExampleFrame } from '@docs-support/example-frame';

export default () => {
  return (
    <ExampleFrame maxWidth="880px" width="100%">
      <Stepper.Root ariaLabel="Registration progress" defaultValue="verification">
        <Stepper.List>
          <Stepper.Item value="profile">
            <Stepper.Indicator />
            <Stepper.Title>Profile</Stepper.Title>
            <Stepper.Description>Basic account details completed.</Stepper.Description>
            <Stepper.Separator />
          </Stepper.Item>
          <Stepper.Item status="error" value="verification">
            <Stepper.Indicator />
            <Stepper.Title>Verification</Stepper.Title>
            <Stepper.Description>We still need a valid confirmation code.</Stepper.Description>
            <Stepper.Separator />
          </Stepper.Item>
          <Stepper.Item disabled value="delivery">
            <Stepper.Indicator />
            <Stepper.Title>Delivery</Stepper.Title>
            <Stepper.Description>Unlocks after verification succeeds.</Stepper.Description>
            <Stepper.Separator />
          </Stepper.Item>
          <Stepper.Item value="review">
            <Stepper.Indicator />
            <Stepper.Title>Review</Stepper.Title>
            <Stepper.Description>Final confirmation before submission.</Stepper.Description>
          </Stepper.Item>
        </Stepper.List>
      </Stepper.Root>
    </ExampleFrame>
  );
};

The component uses semantic list markup, aria-current="step" for the active item, arrow-key navigation that respects the selected orientation, and Home/End support for longer flows. Screen readers also receive hidden state labels for pending, active, completed, error, and disabled steps.

Best Practices

  • Keep step titles short and action-oriented.
  • Use variant="compact" and size="sm" for denser sidebars or narrow containers.
  • Prefer explicit status="error" only when the user must revisit a step.
  • Disable later steps when the flow is strictly sequential.

API

Stepper

PropertyDescriptionTypeDefault Value
ariaLabelAccessible label announced for the navigation landmark.string | undefined'Progress'
classNameMerges custom classes into the root container.string | undefined-
defaultValueSets the initial active step value for uncontrolled usage.string | undefined-
onValueChangeCalled whenever the active step changes.((value: string) => void) | undefined-
valueControls the active step value.string | undefined-
orientationControls the orientation used by the list and keyboard navigation.StepperOrientation | undefined'horizontal'
sizeControls the spacing and sizing scale used by nested parts.StepperSize | undefined'md'
variantApplies a denser or more spacious treatment to nested parts.StepperVariant | undefined'default'