
Build multi-step forms and wizards easily with the Vue Stepper component. Create advanced Vue form flows with custom indicators, validation, and flexible layouts using Vue 3.
Available in Other JavaScript Frameworks
CoreUI Vue Stepper Component – Multi-Step Form Wizard for Vue.js is also available for Angular, Bootstrap, and React. Explore framework-specific implementations below:
The Vue Stepper component helps you build intuitive, multi-step form experiences (Vue Form Wizards) for your Vue.js applications. It supports horizontal and vertical layouts, built-in form validation, customizable indicators, and full slot-based control over step content.
If you’re looking for a Vue Form Wizard or a fully flexible Vue Stepper, this component offers a robust and accessible solution.
Example
This example shows a simple multi-step form wizard built using the Vue Stepper component. Each step defines its content using named slots and optionally provides a native <form> reference for validation. The steps prop defines the sequence, and the @step-change event emits the current index. Internal validation is handled automatically via formRef.
<script setup>
import { ref, useId } from 'vue'
import {
CButton,
CCol,
CFormCheck,
CFormInput,
CFormLabel,
CFormSelect,
CInputGroup,
CInputGroupText,
CPasswordInput,
CStepper,
} from '@coreui/vue-pro'
const stepperRef = ref()
const currentStep = ref(1)
const finish = ref(false)
const uid = useId()
const steps = ['Step 1', 'Step 2', 'Step 3']
</script>
<template>
<div>
<CStepper
:steps="steps"
@finish="finish = true"
@reset="finish = false"
@step-change="(step) => (currentStep = step)"
ref="stepperRef"
>
<template #step-1="{ formRef }">
<form class="row g-3" :ref="formRef">
<CCol :md="4">
<CFormInput type="text" value="Łukasz" label="First name" />
</CCol>
<CCol :md="4">
<CFormInput type="text" value="Holeczek" label="Last name" />
</CCol>
<CCol :md="4">
<CFormLabel :for="`validationCustomUsername-${uid}-01`">Username</CFormLabel>
<CInputGroup>
<CInputGroupText :id="`inputGroupPrependFeedback-${uid}`">@</CInputGroupText>
<CFormInput
type="text"
:id="`validationCustomUsername-${uid}-01`"
:aria-describedby="`inputGroupPrependFeedback-${uid}`"
/>
</CInputGroup>
</CCol>
</form>
</template>
<template #step-2="{ formRef }">
<form class="row g-3" :ref="formRef">
<CCol :md="6">
<CFormInput type="text" label="City" />
</CCol>
<CCol :md="3">
<CFormSelect label="State">
<option disabled>Choose...</option>
<option>...</option>
</CFormSelect>
</CCol>
<CCol :md="3">
<CFormInput type="text" label="Zip" />
</CCol>
</form>
</template>
<template #step-3="{ formRef }">
<form class="row g-3" :ref="formRef">
<CCol :md="6">
<CFormInput type="email" label="Email" />
</CCol>
<CCol :md="6">
<CPasswordInput label="Password" />
</CCol>
<CCol :xs="12">
<CFormCheck type="checkbox" label="Agree to terms and conditions" />
</CCol>
</form>
</template>
</CStepper>
<div v-if="finish">All steps are complete—you're finished.</div>
<div class="d-flex gap-2 mt-4">
<CButton v-if="!finish && currentStep > 1" color="secondary" @click="stepperRef?.prev()">
Previous
</CButton>
<CButton
v-if="!finish && currentStep < steps.length"
color="primary"
@click="stepperRef?.next()"
>
Next
</CButton>
<CButton
v-if="!finish && currentStep === steps.length"
color="primary"
@click="stepperRef?.finish()"
>
Finish
</CButton>
<CButton v-if="finish" color="danger" @click="stepperRef?.reset()"> Reset </CButton>
</div>
</div>
</template> Vertical indicator layout
In this example, the step indicators are stacked vertically above the labels using step-button-layout="vertical", while the form content remains horizontally aligned. This option gives a compact and balanced look, especially useful in narrow layouts.
<template>
<CStepper :steps="['Step 1', 'Step 2', 'Step 3']" stepButtonLayout="vertical" />
</template>
<script setup>
import { CStepper } from '@coreui/vue-pro'
</script> Vertical layout
This example demonstrates a fully vertical Vue Form Wizard. By setting layout="vertical", both the step indicators and the form content are stacked vertically, providing a mobile-friendly top-down progression.
<script setup>
import { ref, useId } from 'vue'
import {
CButton,
CCol,
CFormCheck,
CFormInput,
CFormLabel,
CFormSelect,
CInputGroup,
CInputGroupText,
CPasswordInput,
CStepper,
} from '@coreui/vue-pro'
const stepperRef = ref()
const currentStep = ref(1)
const finish = ref(false)
const uid = useId()
const steps = ['Step 1', 'Step 2', 'Step 3']
</script>
<template>
<div>
<CStepper
:steps="steps"
layout="vertical"
@finish="finish = true"
@reset="finish = false"
@step-change="(step) => (currentStep = step)"
ref="stepperRef"
>
<template #step-1="{ formRef }">
<form class="row g-3 py-3" novalidate :ref="formRef">
<CCol :md="4">
<CFormInput type="text" value="Łukasz" label="First name" />
</CCol>
<CCol :md="4">
<CFormInput type="text" value="Holeczek" label="Last name" />
</CCol>
<CCol :md="4">
<CFormLabel :for="`validationCustomUsername-${uid}-01`">Username</CFormLabel>
<CInputGroup>
<CInputGroupText :id="`inputGroupPrependFeedback-${uid}`">@</CInputGroupText>
<CFormInput
type="text"
:id="`validationCustomUsername-${uid}-01`"
:aria-describedby="`inputGroupPrependFeedback-${uid}`"
/>
</CInputGroup>
</CCol>
</form>
</template>
<template #step-2="{ formRef }">
<form class="row g-3 py-3" novalidate :ref="formRef">
<CCol :md="6">
<CFormInput type="text" label="City" />
</CCol>
<CCol :md="3">
<CFormSelect label="State">
<option disabled>Choose...</option>
<option>...</option>
</CFormSelect>
</CCol>
<CCol :md="3">
<CFormInput type="text" label="Zip" />
</CCol>
</form>
</template>
<template #step-3="{ formRef }">
<form class="row g-3 pt-3" novalidate :ref="formRef">
<CCol :md="6">
<CFormInput type="email" label="Email" />
</CCol>
<CCol :md="6">
<CPasswordInput label="Password" />
</CCol>
<CCol :xs="12">
<CFormCheck type="checkbox" label="Agree to terms and conditions" />
</CCol>
</form>
</template>
</CStepper>
<div v-if="finish">All steps are complete—you're finished.</div>
<div class="d-flex gap-2 mt-4">
<CButton v-if="!finish && currentStep > 1" color="secondary" @click="stepperRef?.prev()">
Previous
</CButton>
<CButton
v-if="!finish && currentStep < steps.length"
color="primary"
@click="stepperRef?.next()"
>
Next
</CButton>
<CButton
v-if="!finish && currentStep === steps.length"
color="primary"
@click="stepperRef?.finish()"
>
Finish
</CButton>
<CButton v-if="finish" color="danger" @click="stepperRef?.reset()"> Reset </CButton>
</div>
</div>
</template> Linear stepper (Form Wizard)
By default, the Vue Stepper operates in linear mode, requiring each step to be completed before the next becomes available. Set :linear="true" to enforce this behavior.
Use a linear Vue Form Wizard when you want structured progression in:
- Checkout flows
- Registration processes
- Multi-step forms with required fields
<template>
<CStepper :steps="['Step 1', 'Step 2', 'Step 3']" />
</template>
<script setup>
import { CStepper } from '@coreui/vue-pro'
</script> Non-linear stepper
You can allow users to skip between steps freely by setting :linear="false". This mode is useful when step order is flexible or optional.
Example use cases:
- Survey wizards
- Product onboarding
- Complex forms with independent sections
<template>
<CStepper :linear="false" :steps="['Step 1', 'Step 2', 'Step 3']" />
</template>
<script setup>
import { CStepper } from '@coreui/vue-pro'
</script> Form validation
The Vue Stepper component provides native step-by-step validation. With :validation="true" (enabled by default), users cannot proceed unless each step’s form is valid. Use the formRef exposed in the step slot to register your native <form>.
Native browser validation
This example demonstrates default HTML5 validation within a Vue Form Wizard.
<script setup>
import { ref, useId } from 'vue'
import {
CButton,
CCol,
CFormCheck,
CFormInput,
CFormLabel,
CFormSelect,
CInputGroup,
CInputGroupText,
CPasswordInput,
CStepper,
} from '@coreui/vue-pro'
const stepperRef = ref()
const currentStep = ref(1)
const finish = ref(false)
const uid = useId()
const steps = ['Step 1', 'Step 2', 'Step 3']
</script>
<template>
<div>
<CStepper
:steps="steps"
@finish="finish = true"
@reset="finish = false"
@step-change="(step) => (currentStep = step)"
ref="stepperRef"
>
<template #step-1="{ formRef }">
<form class="row g-3" :ref="formRef">
<CCol :md="4">
<CFormInput type="text" value="Łukasz" label="First name" required />
</CCol>
<CCol :md="4">
<CFormInput type="text" value="Holeczek" label="Last name" required />
</CCol>
<CCol :md="4">
<CFormLabel :for="`validationCustomUsername-${uid}-01`">Username</CFormLabel>
<CInputGroup>
<CInputGroupText :id="`inputGroupPrependFeedback-${uid}`">@</CInputGroupText>
<CFormInput
type="text"
:id="`validationCustomUsername-${uid}-01`"
:aria-describedby="`inputGroupPrependFeedback-${uid}`"
required
/>
</CInputGroup>
</CCol>
</form>
</template>
<template #step-2="{ formRef }">
<form class="row g-3" :ref="formRef">
<CCol :md="6">
<CFormInput type="text" label="City" />
</CCol>
<CCol :md="3">
<CFormSelect label="State">
<option disabled>Choose...</option>
<option>...</option>
</CFormSelect>
</CCol>
<CCol :md="3">
<CFormInput type="text" label="Zip" />
</CCol>
</form>
</template>
<template #step-3="{ formRef }">
<form class="row g-3" :ref="formRef">
<CCol :md="6">
<CFormInput type="email" label="Email" required />
</CCol>
<CCol :md="6">
<CPasswordInput label="Password" required />
</CCol>
<CCol :xs="12">
<CFormCheck type="checkbox" label="Agree to terms and conditions" required />
</CCol>
</form>
</template>
</CStepper>
<div v-if="finish">All steps are complete—you're finished.</div>
<div class="d-flex gap-2 mt-4">
<CButton v-if="!finish && currentStep > 1" color="secondary" @click="stepperRef?.prev()">
Previous
</CButton>
<CButton
v-if="!finish && currentStep < steps.length"
color="primary"
@click="stepperRef?.next()"
>
Next
</CButton>
<CButton
v-if="!finish && currentStep === steps.length"
color="primary"
@click="stepperRef?.finish()"
>
Finish
</CButton>
<CButton v-if="finish" color="danger" @click="stepperRef?.reset()"> Reset </CButton>
</div>
</div>
</template> Custom validation
Go beyond native browser validation using @step-validation-complete. This callback allows asynchronous or custom logic (e.g. API validation) before advancing the step.
<script setup>
import { ref, useId } from 'vue'
import {
CButton,
CCol,
CFormCheck,
CFormInput,
CFormLabel,
CFormSelect,
CInputGroup,
CInputGroupText,
CPasswordInput,
CStepper,
} from '@coreui/vue-pro'
const stepperRef = ref()
const currentStep = ref(1)
const validationState = ref(0)
const finish = ref(false)
const uid = useId()
const steps = ['Step 1', 'Step 2', 'Step 3']
</script>
<template>
<div>
<CStepper
:steps="steps"
@finish="finish = true"
@reset="finish = false"
@step-change="(step) => (currentStep = step)"
@step-validation-complete="({ stepNumber }) => (validationState = stepNumber)"
ref="stepperRef"
>
<template #step-1="{ formRef }">
<form
class="row g-3"
:class="{ 'was-validated': validationState === 1 }"
novalidate
:ref="formRef"
>
<CCol :md="4">
<CFormInput type="text" value="Łukasz" label="First name" required />
</CCol>
<CCol :md="4">
<CFormInput type="text" value="Holeczek" label="Last name" required />
</CCol>
<CCol :md="4">
<CFormLabel :for="`validationCustomUsername-${uid}-01`">Username</CFormLabel>
<CInputGroup>
<CInputGroupText :id="`inputGroupPrependFeedback-${uid}`">@</CInputGroupText>
<CFormInput
type="text"
:id="`validationCustomUsername-${uid}-01`"
:aria-describedby="`inputGroupPrependFeedback-${uid}`"
required
/>
</CInputGroup>
</CCol>
</form>
</template>
<template #step-2="{ formRef }">
<form
class="row g-3"
:class="{ 'was-validated': validationState === 2 }"
novalidate
:ref="formRef"
>
<CCol :md="6">
<CFormInput type="text" label="City" />
</CCol>
<CCol :md="3">
<CFormSelect label="State">
<option disabled>Choose...</option>
<option>...</option>
</CFormSelect>
</CCol>
<CCol :md="3">
<CFormInput type="text" label="Zip" />
</CCol>
</form>
</template>
<template #step-3="{ formRef }">
<form
class="row g-3"
:class="{ 'was-validated': validationState === 3 }"
novalidate
:ref="formRef"
>
<CCol :md="6">
<CFormInput type="email" label="Email" required />
</CCol>
<CCol :md="6">
<CPasswordInput label="Password" required />
</CCol>
<CCol :xs="12">
<CFormCheck type="checkbox" label="Agree to terms and conditions" required />
</CCol>
</form>
</template>
</CStepper>
<div v-if="finish">All steps are complete—you're finished.</div>
<div class="d-flex gap-2 mt-4">
<CButton v-if="!finish && currentStep > 1" color="secondary" @click="stepperRef?.prev()">
Previous
</CButton>
<CButton
v-if="!finish && currentStep < steps.length"
color="primary"
@click="stepperRef?.next()"
>
Next
</CButton>
<CButton
v-if="!finish && currentStep === steps.length"
color="primary"
@click="stepperRef?.finish()"
>
Finish
</CButton>
<CButton v-if="finish" color="danger" @click="stepperRef?.reset()"> Reset </CButton>
</div>
</div>
</template> Disable validation
To allow full freedom of movement between steps, simply disable validation by adding :validation="false".
<script setup>
import { ref, useId } from 'vue'
import {
CButton,
CCol,
CFormCheck,
CFormInput,
CFormLabel,
CFormSelect,
CInputGroup,
CInputGroupText,
CPasswordInput,
CStepper,
} from '@coreui/vue-pro'
const stepperRef = ref()
const currentStep = ref(1)
const finish = ref(false)
const uid = useId()
const steps = ['Step 1', 'Step 2', 'Step 3']
</script>
<template>
<div>
<CStepper
:steps="steps"
:validation="false"
@finish="finish = true"
@reset="finish = false"
@step-change="(step) => (currentStep = step)"
ref="stepperRef"
>
<template #step-1="{ formRef }">
<form class="row g-3" :ref="formRef">
<CCol :md="4">
<CFormInput type="text" value="Łukasz" label="First name" required />
</CCol>
<CCol :md="4">
<CFormInput type="text" value="Holeczek" label="Last name" required />
</CCol>
<CCol :md="4">
<CFormLabel :for="`validationCustomUsername-${uid}-01`">Username</CFormLabel>
<CInputGroup>
<CInputGroupText :id="`inputGroupPrependFeedback-${uid}`">@</CInputGroupText>
<CFormInput
type="text"
:id="`validationCustomUsername-${uid}-01`"
:aria-describedby="`inputGroupPrependFeedback-${uid}`"
required
/>
</CInputGroup>
</CCol>
</form>
</template>
<template #step-2="{ formRef }">
<form class="row g-3" :ref="formRef">
<CCol :md="6">
<CFormInput type="text" label="City" />
</CCol>
<CCol :md="3">
<CFormSelect label="State">
<option disabled>Choose...</option>
<option>...</option>
</CFormSelect>
</CCol>
<CCol :md="3">
<CFormInput type="text" label="Zip" />
</CCol>
</form>
</template>
<template #step-3="{ formRef }">
<form class="row g-3" :ref="formRef">
<CCol :md="6">
<CFormInput type="email" label="Email" required />
</CCol>
<CCol :md="6">
<CPasswordInput label="Password" required />
</CCol>
<CCol :xs="12">
<CFormCheck type="checkbox" label="Agree to terms and conditions" required />
</CCol>
</form>
</template>
</CStepper>
<div v-if="finish">All steps are complete—you're finished.</div>
<div class="d-flex gap-2 mt-4">
<CButton v-if="!finish && currentStep > 1" color="secondary" @click="stepperRef?.prev()">
Previous
</CButton>
<CButton
v-if="!finish && currentStep < steps.length"
color="primary"
@click="stepperRef?.next()"
>
Next
</CButton>
<CButton
v-if="!finish && currentStep === steps.length"
color="primary"
@click="stepperRef?.finish()"
>
Finish
</CButton>
<CButton v-if="finish" color="danger" @click="stepperRef?.reset()"> Reset </CButton>
</div>
</div>
</template>