Skip to content

Stepper

An opinionated component that lets you navigate through a series of steps and substeps.

Basic Usage

In order to navigate throughout the steps, the Stepper exposes the next and back methods, and whenever the next method is called, it will set the current step as completed before moving to the next one.

What you need to know
vue
<script setup lang="ts">
  import { ref } from 'vue';

  import Button from '../../../src/components/Button/Button.vue';
  import Step from '../../../src/components/Step/Step.vue';
  import Stepper from '../../../src/components/Stepper/Stepper.vue';

  const stepIndex = ref(0);

  const steps = [
    { text: 'Getting Started', step: '1' },
    { text: 'Account Info', step: '2' },
    { text: 'Personal Info', step: '3' },
    { text: 'Verification', step: '4' },
  ];

  const stepperRef = ref<InstanceType<typeof Stepper>>();
  const isComplete = ref(false);

  function handleNextStep() {
    if (stepIndex.value === steps.length - 1) {
      isComplete.value = true;
      stepIndex.value = -1;
    }

    stepperRef.value?.next();
  }
</script>

<template>
  <div class="tw-flex tw-bg-white">
    <Stepper ref="stepperRef" v-model:step="stepIndex" class="tw-w-[200px] tw-bg-purple-500 tw-py-3 tw-pl-3">
      <Step
        v-for="(step, index) in steps"
        :key="step.text"
        :text="step.text"
        :step="step.step"
        :active="stepperRef?.isStepActive(index)"
        :completed="stepperRef?.isStepCompleted(index)"
        @click="stepperRef?.goTo(index)"
      ></Step>
    </Stepper>

    <div class="tw-grid tw-flex-1 tw-place-items-center">
      <span v-if="stepIndex === 0">What you need to know</span>
      <span v-if="stepIndex === 1">Provide your account information</span>
      <span v-if="stepIndex === 2">Tell us more about you</span>
      <span v-if="stepIndex === 3">Verify your account</span>
      <span v-if="isComplete && stepIndex === -1">All done!</span>
    </div>
  </div>

  <div class="tw-mt-6 tw-flex tw-w-full tw-justify-between">
    <Button :disabled="stepIndex <= 0" @click="stepperRef?.back()">Back</Button>
    <Button :disabled="isComplete && stepIndex === -1" @click="handleNextStep">Next</Button>
  </div>
</template>

Orientation

By default, the Stepper is rendered vertically, but you can change it to horizontal by setting the orientation prop to horizontal.

Provide your account information
vue
<script setup lang="ts">
  import { ref } from 'vue';

  import Button from '../../../src/components/Button/Button.vue';
  import Step from '../../../src/components/Step/Step.vue';
  import Stepper from '../../../src/components/Stepper/Stepper.vue';

  const stepIndex = ref(0);

  const steps = [
    { text: 'Account info', step: '1' },
    { text: 'Personal Info', step: '2' },
    { text: 'Verification', step: '3' },
  ];

  const stepperRef = ref<InstanceType<typeof Stepper>>();
  const isComplete = ref(false);

  function handleNextStep() {
    if (stepIndex.value === steps.length - 1) {
      isComplete.value = true;
      stepIndex.value = -1;
    }

    stepperRef.value?.next();
  }
</script>

<template>
  <div class="tw-flex tw-flex-col tw-bg-white">
    <div class="tw-bg-purple-500 tw-p-4">
      <Stepper ref="stepperRef" v-model:step="stepIndex" orientation="horizontal">
        <Step
          v-for="(step, index) in steps"
          :key="step.text"
          :text="step.text"
          :step="step.step"
          :active="stepperRef?.isStepActive(index)"
          :completed="stepperRef?.isStepCompleted(index)"
          @click="stepperRef?.goTo(index)"
        ></Step>
      </Stepper>
    </div>

    <div class="tw-grid tw-min-h-[200px] tw-flex-1 tw-place-items-center">
      <span v-if="stepIndex === 0">Provide your account information</span>
      <span v-if="stepIndex === 1">Tell us more about you</span>
      <span v-if="stepIndex === 2">Verify your account</span>
      <span v-if="isComplete && stepIndex === -1">All done!</span>
    </div>
  </div>

  <div class="tw-mt-6 tw-flex tw-w-full tw-justify-between">
    <Button :disabled="stepIndex <= 0" @click="stepperRef?.back()">Back</Button>
    <Button :disabled="isComplete && stepIndex === -1" @click="handleNextStep">Next</Button>
  </div>
</template>

Horizontal Responsiveness

The horizontal Stepper will render one step at a time for the sm and md breakpoints (resize the viewport to see it in action, above). Navigation arrows are displayed as assistive visual indicators, reflective of whether the user can freely move to the adjacent steps depending on step index and step status. This will support any number of top-level steps, and is not intended for use with nested steps. To see an example of the responsive horizontal stepper using the light theme, see theming.

Manually applying responsive layout

In some cases, a stepper might be needed to be rendered in a responsive way, due to layout constraints (such as a modal). To achieve it, you can set an optional prop useResponsiveNav to true. By doing this, the horizontal stepper will be always rendered with the responsive layout.

Provide your account information

Linearity

By default, the Stepper is not linear, meaning that you can navigate through the steps in any order. However, you can change it by setting the linear prop to true. Doing this will prevent navigating to incomplete steps.

Provide your account information
vue
<script setup lang="ts">
  import { ref } from 'vue';

  import Button from '../../../src/components/Button/Button.vue';
  import Step from '../../../src/components/Step/Step.vue';
  import Stepper from '../../../src/components/Stepper/Stepper.vue';

  const stepIndex = ref(0);

  const steps = [
    { text: 'Account Info', step: '1' },
    { text: 'Personal Info', step: '2' },
    { text: 'Verification', step: '3' },
  ];

  const stepperRef = ref<InstanceType<typeof Stepper>>();
  const isComplete = ref(false);

  function handleNextStep() {
    if (stepIndex.value === steps.length - 1) {
      isComplete.value = true;
      stepIndex.value = -1;
    }

    stepperRef.value?.next();
  }
</script>

<template>
  <div class="tw-flex tw-bg-white">
    <Stepper ref="stepperRef" v-model:step="stepIndex" linear class="tw-w-[200px] tw-bg-purple-500 tw-py-3 tw-pl-3">
      <Step
        v-for="(step, index) in steps"
        :key="step.text"
        :text="step.text"
        :step="step.step"
        :active="stepperRef?.isStepActive(index)"
        :completed="stepperRef?.isStepCompleted(index)"
        :disabled="index > 0 && !stepperRef?.isStepCompleted(index - 1)"
        @click="stepperRef?.goTo(index)"
      ></Step>
    </Stepper>

    <div class="tw-grid tw-flex-1 tw-place-items-center">
      <span v-if="stepIndex === 0">Provide your account information</span>
      <span v-if="stepIndex === 1">Tell us more about you</span>
      <span v-if="stepIndex === 2">Verify your account</span>
      <span v-if="isComplete && stepIndex === -1">All done!</span>
    </div>
  </div>

  <div class="tw-mt-6 tw-flex tw-w-full tw-justify-between">
    <Button :disabled="stepIndex <= 0" @click="stepperRef?.back()">Back</Button>
    <Button :disabled="isComplete && stepIndex === -1" @click="handleNextStep">Next</Button>
  </div>
</template>

Substeps

You can also add substeps, which are nested steps. Whenever a parent step becomes active, its first substep will become active as well.

{ activeStep: 0, activeSubstep: 0 }
vue
<script setup lang="ts">
  import { ref } from 'vue';

  import Button from '../../../src/components/Button/Button.vue';
  import Step from '../../../src/components/Step/Step.vue';
  import Stepper from '../../../src/components/Stepper/Stepper.vue';

  const stepperRef = ref<InstanceType<typeof Stepper>>();

  const steps = [
    {
      text: 'First step',
      step: '1',
      substeps: [
        {
          text: 'First substep',
        },
        {
          text: 'Second substep',
        },
        {
          text: 'Third substep',
        },
      ],
    },
    {
      text: 'Second step',
      step: '2',
      substeps: [
        {
          text: 'First substep',
        },
        {
          text: 'Second substep',
        },
        {
          text: 'Third substep',
        },
      ],
    },
    {
      text: 'Third step',
      step: '3',
    },
    {
      text: 'Fourth step',
      step: '4',
    },
  ];

  const activeStep = ref(0);
  const activeSubstep = ref(0);
</script>

<template>
  <div class="tw-flex tw-bg-white">
    <Stepper
      ref="stepperRef"
      v-model:step="activeStep"
      v-model:substep="activeSubstep"
      class="tw-w-[200px] tw-bg-purple-500 tw-p-6"
    >
      <Step
        v-for="(step, stepIndex) in steps"
        :key="step.text"
        :text="step.text"
        :step="step.step"
        :active="stepperRef?.isStepActive(stepIndex)"
        :completed="stepperRef?.isStepCompleted(stepIndex)"
        @click="stepperRef?.goTo(stepIndex)"
      >
        <Step
          v-for="(substep, substepIndex) in step.substeps"
          :key="substep.text"
          :nested="true"
          :text="substep.text"
          :active="stepperRef?.isStepActive(stepIndex, substepIndex)"
          :completed="stepperRef?.isStepCompleted(stepIndex, substepIndex)"
          @click.stop="stepperRef?.goTo(stepIndex, substepIndex)"
        ></Step>
      </Step>
    </Stepper>

    <div class="tw-grid tw-flex-1 tw-place-items-center">
      <code> { activeStep: {{ activeStep }}, activeSubstep: {{ activeSubstep }} } </code>
    </div>
  </div>

  <div class="tw-mt-6 tw-flex tw-w-full tw-justify-between">
    <Button :disabled="activeStep < 0" @click="stepperRef?.back()">Back</Button>
    <Button :disabled="activeStep === steps.length - 1" @click="stepperRef?.next()">Next</Button>
  </div>
</template>

Theming

By default, the Stepper is suitable for darker backgrounds. On brighter backgrounds, you can switch the theme prop to light.

Provide your account information
vue
<script setup lang="ts">
  import { ref } from 'vue';

  import Button from '../../../src/components/Button/Button.vue';
  import Step from '../../../src/components/Step/Step.vue';
  import Stepper from '../../../src/components/Stepper/Stepper.vue';

  const stepIndex = ref(0);

  const steps = [
    { text: 'Account info', step: '1' },
    { text: 'Personal Info', step: '2' },
    { text: 'Verification', step: '3' },
  ];

  const _props = withDefaults(defineProps<{ useResponsiveNav?: boolean }>(), {
    useResponsiveNav: false,
  });

  const stepperRef = ref<InstanceType<typeof Stepper>>();
  const isComplete = ref(false);

  function handleNextStep() {
    if (stepIndex.value === steps.length - 1) {
      isComplete.value = true;
      stepIndex.value = -1;
    }

    stepperRef.value?.next();
  }
</script>

<template>
  <div class="tw-flex tw-flex-col tw-bg-white">
    <div class="tw-p-4">
      <Stepper
        ref="stepperRef"
        v-model:step="stepIndex"
        orientation="horizontal"
        theme="light"
        :use-responsive-nav="useResponsiveNav"
      >
        <Step
          v-for="(step, index) in steps"
          :key="step.text"
          :text="step.text"
          :step="step.step"
          :active="stepperRef?.isStepActive(index)"
          :completed="stepperRef?.isStepCompleted(index)"
          @click="stepperRef?.goTo(index)"
        ></Step>
      </Stepper>
    </div>

    <div class="tw-grid tw-min-h-[200px] tw-flex-1 tw-place-items-center">
      <span v-if="stepIndex === 0">Provide your account information</span>
      <span v-if="stepIndex === 1">Tell us more about you</span>
      <span v-if="stepIndex === 2">Verify your account</span>
      <span v-if="isComplete && stepIndex === -1">All done!</span>
    </div>
  </div>

  <div class="tw-mt-6 tw-flex tw-w-full tw-justify-between">
    <Button :disabled="stepIndex <= 0" @click="stepperRef?.back()">Back</Button>
    <Button :disabled="isComplete && stepIndex === -1" @click="handleNextStep">Next</Button>
  </div>
</template>

Step

A Step is a component that represents a single step or substep in the Stepper.

Active

template
<Step text="Active step" step="1" active />

Loading

template
<Step text="Loading step" step="1" loading />

Error

template
<Step text="Error step" step="1" error />

Completed

template
<Step text="Completed step" step="1" completed />

Disabled

template
<Step text="Completed step" step="1" disabled />

Nested

In order to display substeps of a step, the Step component accepts the nested prop. This prop will display a line connecting the parent step to the nested step.

template
<Step text="Parent step" step="A" active>
  <Step nested text="Nested step" active />
</Step>
<Step text="Second step" step="B" />

API

See the documentation below for a complete reference to all the props and classes available to the components mentioned here.