Here's how to build a multi-step form in Vue 3, using the current recommended pattern.
- The core mechanism: a single index (current step) plus v-show to toggle visibility
vue
<!-- MultiStepForm.vue -->
<script setup>
import { ref } from 'vue'
const activeStepIndex = ref(0)
const totalSteps = 3
const formData = ref({
- name: '',
- email: '',
- street: '',
- city: '',
})
function nextStep() {
- if (activeStepIndex.value < totalSteps — 1) activeStepIndex.value++
}
function prevStep() {
if (activeStepIndex.value > 0) activeStepIndex.value--
}
function submit() {
console.log(formData.value)
// POST formData to your API here
}
</script>
<template>
<form @submit.prevent="submit">
<!-- Step indicator -->
<div class="steps">
<div
v-for="n in totalSteps"
:key="n"
- :class="{ active: n — 1 === activeStepIndex }"
>
{{ n }}
</div>
</div>
<!-- Step 1 -->
<div v-show="activeStepIndex === 0">
<input v-model="formData.name" placeholder="Name" />
<input v-model="formData.email" placeholder="Email" />
</div>
<!-- Step 2 -->
<div v-show="activeStepIndex === 1">
<input v-model="formData.street" placeholder="Street" />
<input v-model="formData.city" placeholder="City" />
</div>
-
<!-- Step 3: review/confirm -->
<div v-show="activeStepIndex === 2">
-
<p>Name: {{ formData.name }}</p>
-
<p>Email: {{ formData.email }}</p>
</div>
<footer>
<button type="button" @click="prevStep" v-if="activeStepIndex > 0">Previous</button>
<button type="button" @click="nextStep" v-if="activeStepIndex < totalSteps - 1">Next</button>
<button type="submit" v-else>Submit</button>
</footer>
</form>
</template>
Key design decisions worth understanding
-
All form data lives in one formData object, separate from UI state (activeStepIndex) — this gives a clean separation between "what step am I on" and "what has the user entered," and means the full data set is available at submission regardless of which step it was entered on
-
v-show rather than v-if for steps — v-show keeps all steps in the DOM and just toggles CSS visibility, which preserves each step's input state as you navigate back and forth. If you used v-if, Vue would destroy and recreate the DOM elements on each step change, and any local component state not bound to your shared formData object would reset
-
A single <form> wrapping all steps, with only the active step's inputs visible — this way @submit.prevent and native form semantics still work correctly on the final step
-
Making it reusable with slots — the pattern for a genuinely reusable multi-step component
Rather than hardcoding each step's markup inside one component, use named slots so the parent controls what each step contains:
vue
<!-- MultiStepForm.vue (reusable) -->
<template>
<form @submit.prevent="submitStep">
<div
v-for="(step, index) in steps"
:key="index"
v-show="activeStepIndex === index"
>
<slot :name="'step' + (index + 1)" />
</div>
<button type="submit">Next</button>
</form>
</template>
<script setup>
import { ref } from 'vue'
defineProps(['steps'])
const activeStepIndex = ref(0)
</script>
-
This lets any parent component define wildly different content per step (different field sets, different layouts) while reusing the same navigation/state logic — genuinely the right pattern once you have more than one multi-step form in your app.
-
Validation — gate step transitions, don't just let users click through
-
For a production form, add a check in nextStep() that validates the current step's fields before advancing (rather than trusting the user got everything right) — pairing this with VeeValidate + Zod (covered in earlier Vue validation questions in this conversation) is the standard approach: validate the current step's schema slice before incrementing activeStepIndex.
If you want this handled for you rather than building it by hand
Vueform (covered in the previous "complex forms" question) has native step support via <FormSteps>/<FormStep> components, including conditional steps and automatic exclusion of hidden-step data from the submission payload
- vue-multi-step-form (npm package) — a ready-made slot-based multi-step component if you want the reusable pattern above without building it yourself
Practical recommendation
Simple, one-off multi-step form → the v-show + shared formData pattern above, hand-built
Multiple multi-step forms across your app, want reusability → the slot-based component pattern, or the vue-multi-step-form package
Need step-level conditional logic (skip a step based on a prior answer) and built-in validation → Vueform