Overview
The createStep composable extendscreateSingle with navigation methods for sequential traversal. Supports both circular (wrapping) and bounded (stopping at edges) navigation modes.
Perfect for wizards, carousels, pagination, and onboarding flows.
Signature
function createStep<
Z extends StepTicketInput = StepTicketInput,
E extends StepTicket<Z> = StepTicket<Z>
>(options?: StepOptions): StepContext<Z, E>
Configuration options
Show properties
Show properties
Enable circular navigation (wrapping at boundaries):
true: Navigation wraps around (carousel behavior)false: Navigation stops at boundaries (pagination behavior)
Mandatory selection enforcement (inherited from createSingle)
Disable entire stepper
Step navigation instance
Show properties
Show properties
Select the first non-disabled item
Select the last non-disabled item
Move to next item (wraps if circular, stops at end if bounded)
Move to previous item (wraps if circular, stops at start if bounded)
Move by count positions (negative for backward, positive for forward)
Currently selected item ID (from createSingle)
Currently selected item (from createSingle)
Currently selected item index (from createSingle)
Currently selected item value (from createSingle)
Usage
Basic Navigation
import { createStep } from '@vuetify/v0'
const wizard = createStep()
wizard.onboard([
{ id: 'step-1', value: 'Personal Info' },
{ id: 'step-2', value: 'Address' },
{ id: 'step-3', value: 'Payment' },
{ id: 'step-4', value: 'Review' },
])
wizard.first()
console.log(wizard.selectedIndex.value) // 0
wizard.next()
console.log(wizard.selectedIndex.value) // 1
wizard.last()
console.log(wizard.selectedIndex.value) // 3
wizard.prev()
console.log(wizard.selectedIndex.value) // 2
Bounded Navigation (Default)
const pagination = createStep({ circular: false })
pagination.onboard([
{ id: 'page-1', value: 1 },
{ id: 'page-2', value: 2 },
{ id: 'page-3', value: 3 },
])
pagination.first()
console.log(pagination.selectedIndex.value) // 0
pagination.prev() // Does nothing - already at first
console.log(pagination.selectedIndex.value) // 0
pagination.last()
pagination.next() // Does nothing - already at last
console.log(pagination.selectedIndex.value) // 2
Circular Navigation
const carousel = createStep({ circular: true })
carousel.onboard([
{ id: 'slide-1', value: 'Slide 1' },
{ id: 'slide-2', value: 'Slide 2' },
{ id: 'slide-3', value: 'Slide 3' },
])
carousel.first()
console.log(carousel.selectedIndex.value) // 0
carousel.prev() // Wraps to last
console.log(carousel.selectedIndex.value) // 2
carousel.next() // Wraps to first
console.log(carousel.selectedIndex.value) // 0
Step by Count
const stepper = createStep()
stepper.onboard([
{ id: 'step-1', value: 'Step 1' },
{ id: 'step-2', value: 'Step 2' },
{ id: 'step-3', value: 'Step 3' },
{ id: 'step-4', value: 'Step 4' },
{ id: 'step-5', value: 'Step 5' },
])
stepper.first()
stepper.step(2) // Jump forward 2 steps
console.log(stepper.selectedIndex.value) // 2
stepper.step(-1) // Jump back 1 step
console.log(stepper.selectedIndex.value) // 1
Skip Disabled Items
const stepper = createStep()
stepper.onboard([
{ id: 'step-1', value: 'Step 1' },
{ id: 'step-2', value: 'Step 2', disabled: true },
{ id: 'step-3', value: 'Step 3', disabled: true },
{ id: 'step-4', value: 'Step 4' },
])
stepper.first()
console.log(stepper.selectedIndex.value) // 0
stepper.next() // Skips disabled items
console.log(stepper.selectedIndex.value) // 3 (jumped to step-4)
Wizard Component
<script setup lang="ts">
import { createStep } from '@vuetify/v0'
const wizard = createStep({ mandatory: 'force' })
wizard.onboard([
{ id: 'step-1', value: 'Account Details' },
{ id: 'step-2', value: 'Profile Info' },
{ id: 'step-3', value: 'Preferences' },
{ id: 'step-4', value: 'Confirmation' },
])
function isFirstStep() {
return wizard.selectedIndex.value === 0
}
function isLastStep() {
return wizard.selectedIndex.value === wizard.size - 1
}
</script>
<template>
<div class="wizard">
<div class="steps">
<div
v-for="(step, index) in wizard.values()"
:key="step.id"
:class="{
active: step.isSelected.value,
completed: index < wizard.selectedIndex.value
}"
>
{{ step.value }}
</div>
</div>
<div class="content">
<p>Current step: {{ wizard.selectedValue }}</p>
</div>
<div class="actions">
<button
@click="wizard.prev()"
:disabled="isFirstStep()"
>
Previous
</button>
<button
@click="wizard.next()"
:disabled="isLastStep()"
>
{{ isLastStep() ? 'Finish' : 'Next' }}
</button>
</div>
</div>
</template>
Carousel Component
<script setup lang="ts">
import { createStep } from '@vuetify/v0'
import { onMounted, onUnmounted } from 'vue'
const carousel = createStep({
circular: true,
mandatory: 'force'
})
carousel.onboard([
{ id: 'slide-1', value: 'Slide 1' },
{ id: 'slide-2', value: 'Slide 2' },
{ id: 'slide-3', value: 'Slide 3' },
])
// Auto-advance every 3 seconds
let interval: number
onMounted(() => {
interval = setInterval(() => {
carousel.next()
}, 3000)
})
onUnmounted(() => {
clearInterval(interval)
})
</script>
<template>
<div class="carousel">
<button @click="carousel.prev()">←</button>
<div class="slides">
<div
v-for="slide in carousel.values()"
:key="slide.id"
v-show="slide.isSelected.value"
>
{{ slide.value }}
</div>
</div>
<button @click="carousel.next()">→</button>
<div class="indicators">
<span
v-for="slide in carousel.values()"
:key="slide.id"
:class="{ active: slide.isSelected.value }"
@click="carousel.select(slide.id)"
/>
</div>
</div>
</template>
Type Safety
interface WizardStep extends StepTicketInput {
title: string
description?: string
completed?: boolean
}
const wizard = createStep<WizardStep>()
wizard.onboard([
{ title: 'Welcome', description: 'Get started' },
{ title: 'Setup', description: 'Configure' },
{ title: 'Done', completed: true },
])
// Type-safe access
const currentStep = wizard.selectedItem.value
if (currentStep) {
console.log(currentStep.title) // string
console.log(currentStep.completed) // boolean | undefined
}
Circular vs Bounded
| Feature | Circular (true) | Bounded (false, default) |
|---|---|---|
next() at end | Wraps to first | Stays at last |
prev() at start | Wraps to last | Stays at first |
step(100) | Uses modulo wrapping | Stops at boundary |
| Use case | Carousels, infinite scroll | Wizards, pagination |
Edge Cases
const stepper = createStep()
stepper.onboard([
{ id: 'step-1', value: 'Step 1', disabled: true },
{ id: 'step-2', value: 'Step 2' },
{ id: 'step-3', value: 'Step 3', disabled: true },
])
// Only one enabled item
stepper.first()
console.log(stepper.selectedId.value) // 'step-2'
stepper.next() // Stays at step-2 (no enabled items after)
console.log(stepper.selectedId.value) // 'step-2'
Performance
- Navigation operations: O(1) when no disabled items
- With disabled items: O(n) worst case (must scan for enabled items)
- Circular wrapping: Uses efficient modulo arithmetic
((index % length) + length) % length
Context Pattern
import { createStepContext } from '@vuetify/v0'
export const [useWizard, provideWizard, wizard] = createStepContext()
// In parent component
provideWizard()
// In child component
const wizard = useWizard()
wizard.next()
See Also
- createSingle - Base single-selection system
- createBreadcrumbs - Breadcrumb navigation with path truncation