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.
<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
.
<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.
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.
<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 }
<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
.
<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
<Step text="Active step" step="1" active />
Loading
<Step text="Loading step" step="1" loading />
Error
<Step text="Error step" step="1" error />
Completed
<Step text="Completed step" step="1" completed />
Disabled
<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.
<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.