Stepper

Material Design 3 stepper component for multi-step processes

Stepper

Steppers display progress through a sequence of logical and numbered steps. They guide users through multi-step processes like forms, checkouts, or wizards.

Basic Usage

Basic Stepper

Variants

Horizontal Stepper

The default horizontal layout is ideal for desktop interfaces:

Horizontal Stepper

Vertical Stepper

Vertical steppers work well for mobile devices or when steps require more detail:

Vertical Stepper

Campaign settings content here...

Ad group content here...

Step States

Active Step

The current step in the process:

Active Step

Completed Step

Steps that have been successfully completed:

Completed Step

Error Step

Steps with validation errors or issues:

Error Step

Disabled Step

Steps that cannot be accessed yet:

Disabled Step

Optional Step

Mark steps as optional:

Optional Step

Color Variants

Primary (Default)

Primary Stepper

Secondary

Secondary Stepper

Tertiary

Tertiary Stepper

With Descriptions

Add descriptions to provide context for each step:

Stepper with Descriptions

Interactive Example

Here’s a complete example with step content:

Interactive Stepper Example

Best Practices

Step Count

  • 3-5 steps: Ideal for most processes
  • More than 5 steps: Consider grouping steps or using a different pattern
  • 2 steps: Consider using tabs or a simpler layout

Labels

  • Keep step labels short and clear (1-2 words)
  • Use action verbs when possible
  • Ensure labels accurately describe the step content
  • Allow users to navigate back to previous steps
  • Consider whether to allow skipping ahead
  • Always validate before moving to the next step

Accessibility

Accessible Stepper

Mobile Considerations

Responsive Stepper

Framework Examples

React

interface Step {
  label: string;
  description?: string;
  optional?: boolean;
}

interface StepperProps {
  steps: Step[];
  activeStep: number;
  orientation?: 'horizontal' | 'vertical';
  color?: 'primary' | 'secondary' | 'tertiary';
  onStepClick?: (step: number) => void;
}

export function Stepper({
  steps,
  activeStep,
  orientation = 'horizontal',
  color = 'primary',
  onStepClick
}: StepperProps) {
  const stepperClass = `stepper ${
    orientation === 'vertical' ? 'stepper-vertical' : ''
  } ${color !== 'primary' ? `stepper-${color}` : ''}`;

  return (
    <div className={stepperClass}>
      {steps.map((step, index) => {
        const isActive = index === activeStep;
        const isCompleted = index < activeStep;

        const stepClass = `stepper-step ${
          isActive ? 'stepper-step-active' : ''
        } ${isCompleted ? 'stepper-step-completed' : ''} ${
          step.optional ? 'stepper-step-optional' : ''
        }`;

        return (
          <div key={index} className={stepClass}>
            <button
              className="stepper-step-button"
              onClick={() => onStepClick?.(index)}
              disabled={index > activeStep}
            >
              <div className="stepper-step-icon">
                {isCompleted ? '✓' : index + 1}
              </div>
              <div>
                <span className="stepper-step-label">{step.label}</span>
                {step.description && (
                  <div className="stepper-step-description">
                    {step.description}
                  </div>
                )}
              </div>
            </button>
            {index < steps.length - 1 && (
              <div className="stepper-step-connector" />
            )}
          </div>
        );
      })}
    </div>
  );
}

// Usage
const steps = [
  { label: 'Account', description: 'Basic info' },
  { label: 'Profile', description: 'Personal details' },
  { label: 'Confirm', description: 'Review', optional: true }
];

<Stepper steps={steps} activeStep={1} orientation="vertical" />

Vue

<template>
  <div :class="stepperClass">
    <div
      v-for="(step, index) in steps"
      :key="index"
      :class="getStepClass(index)"
    >
      <button
        class="stepper-step-button"
        @click="$emit('step-click', index)"
        :disabled="index > activeStep"
      >
        <div class="stepper-step-icon">
          {{ index < activeStep ? '✓' : index + 1 }}
        </div>
        <div>
          <span class="stepper-step-label">{{ step.label }}</span>
          <div
            v-if="step.description"
            class="stepper-step-description"
          >
            {{ step.description }}
          </div>
        </div>
      </button>
      <div
        v-if="index < steps.length - 1"
        class="stepper-step-connector"
      />
    </div>
  </div>
</template>

<script setup>
import { computed } from 'vue';

const props = defineProps({
  steps: {
    type: Array,
    required: true
  },
  activeStep: {
    type: Number,
    default: 0
  },
  orientation: {
    type: String,
    default: 'horizontal'
  },
  color: {
    type: String,
    default: 'primary'
  }
});

const stepperClass = computed(() => {
  return `stepper ${
    props.orientation === 'vertical' ? 'stepper-vertical' : ''
  } ${props.color !== 'primary' ? `stepper-${props.color}` : ''}`;
});

const getStepClass = (index) => {
  const isActive = index === props.activeStep;
  const isCompleted = index < props.activeStep;
  const step = props.steps[index];

  return `stepper-step ${
    isActive ? 'stepper-step-active' : ''
  } ${isCompleted ? 'stepper-step-completed' : ''} ${
    step.optional ? 'stepper-step-optional' : ''
  }`;
};
</script>

<!-- Usage -->
<Stepper
  :steps="[
    { label: 'Account', description: 'Basic info' },
    { label: 'Profile', description: 'Personal details' },
    { label: 'Confirm', description: 'Review', optional: true }
  ]"
  :active-step="1"
  orientation="vertical"
  @step-click="handleStepClick"
/>

API Reference

Container Classes

ClassDescription
.stepperBase stepper container (required)
.stepper-verticalVertical orientation
.stepper-secondarySecondary color variant
.stepper-tertiaryTertiary color variant
.stepper-alt-labelsAlternative label positioning

Step Classes

ClassDescription
.stepper-stepIndividual step container (required)
.stepper-step-activeCurrent active step
.stepper-step-completedCompleted step
.stepper-step-errorStep with error
.stepper-step-disabledDisabled step
.stepper-step-optionalOptional step (shows “(Optional)” label)

Step Element Classes

ClassDescription
.stepper-step-buttonClickable step button
.stepper-step-iconStep number or icon circle
.stepper-step-labelStep label text
.stepper-step-descriptionOptional step description
.stepper-step-connectorLine connecting steps
.stepper-step-contentContent area (for vertical stepper)

Structure

<div class="stepper [stepper-vertical] [stepper-{color}]">
  <div class="stepper-step [stepper-step-{state}]">
    <button class="stepper-step-button">
      <div class="stepper-step-icon">{number|icon}</div>
      <div>
        <span class="stepper-step-label">{label}</span>
        <div class="stepper-step-description">{description}</div>
      </div>
    </button>
    <div class="stepper-step-connector"></div>
    <div class="stepper-step-content">{content}</div>
  </div>
</div>
  • Tabs - Alternative navigation pattern
  • Breadcrumb - Hierarchical navigation
  • Progress - Linear progress indicator
  • Button - Action buttons for navigation

See Also