Skip to main content
You are viewing Pre-Alpha documentation.
Vuetify0 Logo
Theme
Mode
Accessibility
Vuetify

Sign in

Sign in with your preferred provider to access your account.

createStep

A composable for managing navigation through multi-step processes like forms, wizards, or onboarding flows, with support for step tracking, completion, and navigation controls.


Intermediate100% coverageFeb 4, 2026

Usage

The createStep composable manages a list of steps and allows navigation between them with configurable circular (wrapping) or bounded (stopping at edges) behavior. You register each step (with an id and value) in the order they should be navigated, then use the navigation methods to move

ts
import { createStep } from '@vuetify/v0'

// Bounded navigation (default) - for wizards, forms
const wizard = createStep({ circular: false })

wizard.onboard([
  { id: 'step1', value: 'Account Info' },
  { id: 'step2', value: 'Payment' },
  { id: 'step3', value: 'Confirmation' },
])

wizard.first()    // Go to step1
wizard.next()     // Go to step2
wizard.next()     // Go to step3
wizard.next()     // Stays at step3 (bounded)

// Circular navigation - for carousels, theme switchers
const carousel = createStep({ circular: true })

carousel.onboard([
  { id: 'slide1', value: 'First' },
  { id: 'slide2', value: 'Second' },
  { id: 'slide3', value: 'Third' },
])

carousel.last()   // Go to slide3
carousel.next()   // Wraps to slide1
carousel.prev()   // Wraps to slide3

Examples

1
Cart
2
Shipping
3
Payment
4
Review
5
Confirm

Step 1 of 5 · Payment step is disabled (auto-skipped)

<script setup lang="ts">
  import { createStep } from '@vuetify/v0'
  import { computed } from 'vue'

  const steps = [
    { id: 'cart', label: 'Cart', icon: '1' },
    { id: 'shipping', label: 'Shipping', icon: '2' },
    { id: 'payment', label: 'Payment', icon: '3', disabled: true },
    { id: 'review', label: 'Review', icon: '4' },
    { id: 'confirm', label: 'Confirm', icon: '5' },
  ]

  const stepper = createStep()
  stepper.onboard(steps.map((s, i) => ({ id: s.id, value: i, disabled: s.disabled })))
  stepper.first()

  const currentIndex = computed(() => stepper.selectedIndex.value)
  const isFirst = computed(() => currentIndex.value === 0)
  const isLast = computed(() => currentIndex.value === stepper.size - 1)
</script>

<template>
  <div class="w-full max-w-2xl mx-auto py-8">
    <!-- Stepper Track -->
    <div class="relative flex items-center justify-between mb-12">
      <!-- Connecting Line (Background) -->
      <div class="absolute top-5 left-0 right-0 h-0.5 bg-divider" />

      <!-- Progress Line -->
      <div
        class="absolute top-5 left-0 h-0.5 bg-primary transition-all duration-500 ease-out"
        :style="{ width: `${(currentIndex / (steps.length - 1)) * 100}%` }"
      />

      <!-- Steps -->
      <div
        v-for="(step, i) in steps"
        :key="step.id"
        class="relative z-10 flex flex-col items-center cursor-pointer group"
        @click="!step.disabled && stepper.select(step.id)"
      >
        <!-- Circle -->
        <div
          class="w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold transition-all duration-300"
          :class="[
            i < currentIndex ? 'bg-primary text-on-primary scale-90' : '',
            i === currentIndex ? 'bg-primary text-on-primary ring-4 ring-primary/30 scale-110' : '',
            i > currentIndex && !step.disabled ? 'bg-surface border-2 border-divider text-on-surface-variant group-hover:border-primary' : '',
            step.disabled ? 'bg-surface border-2 border-dashed border-divider text-on-surface-variant/50 cursor-not-allowed' : '',
          ]"
        >
          <template v-if="i < currentIndex">
            <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path d="M5 13l4 4L19 7" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" />
            </svg>
          </template>
          <template v-else>{{ step.icon }}</template>
        </div>

        <!-- Label -->
        <span
          class="mt-3 text-xs font-medium transition-colors duration-200"
          :class="[
            i === currentIndex ? 'text-primary' : 'text-on-surface-variant',
            step.disabled ? 'line-through opacity-50' : '',
          ]"
        >
          {{ step.label }}
        </span>
      </div>
    </div>

    <!-- Controls -->
    <div class="flex items-center justify-center gap-2">
      <button
        class="px-3 py-1.5 text-sm rounded border border-divider hover:bg-surface-tint disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
        :disabled="isFirst"
        @click="stepper.first()"
      >
        First
      </button>
      <button
        class="px-4 py-1.5 text-sm rounded bg-primary/10 text-primary hover:bg-primary/20 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
        :disabled="isFirst"
        @click="stepper.prev()"
      >
        Prev
      </button>
      <button
        class="px-4 py-1.5 text-sm rounded bg-primary text-on-primary hover:bg-primary/90 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
        :disabled="isLast"
        @click="stepper.next()"
      >
        Next
      </button>
      <button
        class="px-3 py-1.5 text-sm rounded border border-divider hover:bg-surface-tint disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
        :disabled="isLast"
        @click="stepper.last()"
      >
        Last
      </button>
    </div>

    <!-- Status -->
    <p class="mt-6 text-center text-xs text-on-surface-variant">
      Step {{ currentIndex + 1 }} of {{ stepper.size }} &middot; Payment step is disabled (auto-skipped)
    </p>
  </div>
</template>

Architecture

createStep extends createSingle with directional navigation:

Step Navigation Hierarchy

Use controls to zoom and pan. Click outside or press Escape to close.

Step Navigation Hierarchy

Reactivity

Step navigation state is always reactive. Navigation guards (canPrev, canNext) update automatically.

Property/MethodReactiveNotes
selectedIdComputed — current step ID
selectedIndexComputed — current step position
selectedItemComputed — current step ticket
selectedValueComputed — current step value
canPrevComputed — false at first step (bounded mode)
canNextComputed — false at last step (bounded mode)
Tip

Navigation guards Use canPrev and canNext to disable navigation buttons. They respect the circular option automatically.

API Reference

The following API details are for the createStep composable.

Functions

createStep

(_options?: StepOptions) => R

Creates a new step instance with navigation through items. Extends `createSingle` with `first()`, `last()`, `next()`, `prev()`, and `step(count)` methods for sequential navigation. Supports both circular (wrapping) and bounded (stopping at edges) modes.

createStepContext

(_options?: StepContextOptions) => ContextTrinity<R>

Creates a new step context.

useStep

(namespace?: string) => R

Returns the current step instance.

Options

events

boolean

Enable event emission for registry operations

Default: false

reactive

boolean

Enable reactive behavior for registry operations

Default: false

disabled

MaybeRefOrGetter<boolean>

When true, the entire selection instance is disabled.

enroll

MaybeRefOrGetter<boolean>

When true, newly registered items are automatically selected if not disabled. Useful for pre-selecting items in multi-select scenarios.

mandatory

MaybeRefOrGetter<boolean | "force">

Controls mandatory selection behavior: - `false` (default): No mandatory selection enforcement - `true`: Prevents deselecting the last selected item (user must always have one selected) - `'force'`: Automatically selects the first non-disabled item on registration

multiple

MaybeRefOrGetter<boolean>

When true, treats the selection as an array

circular

boolean

Enable circular navigation (wrapping at boundaries). - true: Navigation wraps around (carousel behavior) - false: Navigation stops at boundaries (pagination behavior)

Default: false

Properties

collection

ReadonlyMap<ID, Z>

The collection of tickets in the registry

size

number

The number of tickets in the registry

selectedIds

Reactive<Set<ID>>

Set of selected ticket IDs

selectedItems

ComputedRef<Set<E>>

Set of selected ticket instances

selectedValues

ComputedRef<Set<unknown>>

Set of selected ticket values

disabled

MaybeRef<boolean>

Disable state for the entire selection instance

selectedId

ComputedRef<any>

selectedIndex

ComputedRef<number>

selectedItem

ComputedRef<E>

selectedValue

ComputedRef<E["value"]>

Methods

clear

() => void

Clear the entire registry

has

(id: ID) => boolean

Check if a ticket exists by ID

keys

() => readonly ID[]

Get all registered IDs

browse

(value: Z["value"]) => ID[] | undefined

Browse for an ID(s) by value

lookup

(index: number) => ID | undefined

lookup a ticket by index number

get

(id: ID) => Z | undefined

Get a ticket by ID

upsert

(id: ID, ticket?: Partial<Z>) => Z

Update or insert a ticket by ID

values

() => readonly Z[]

Get all values of registered tickets

entries

() => readonly [ID, Z][]

Get all entries of registered tickets

unregister

(id: ID) => void

Unregister a ticket by ID

reindex

() => void

Reset the index directory and update all tickets

seek

(direction?: "first" | "last", from?: number, predicate?: (ticket) => boolean) => Z | undefined

Seek for a ticket based on direction and optional predicate

on

<K extends Extensible<RegistryEventName>>(event: K, cb: EventHandler<Z, K>) => void

Listen for registry events

off

<K extends Extensible<RegistryEventName>>(event: K, cb: EventHandler<Z, K>) => void

Stop listening for registry events

emit

<K extends Extensible<RegistryEventName>>(event: K, data: EventPayload<Z, K>) => void

Emit an event with data

dispose

() => void

Clears the registry and removes all listeners

offboard

(ids: ID[]) => void

Offboard multiple tickets at once

batch

<R>(fn: () => R) => R

Execute operations in a batch, deferring cache invalidation and event emission until complete

reset

() => void

Clear all selected IDs and reindexes

select

(id: ID) => void

Select a ticket by ID (Toggle ON)

unselect

(id: ID) => void

Unselect a ticket by ID (Toggle OFF)

toggle

(id: ID) => void

Toggles a ticket ON and OFF by ID

selected

(id: ID) => boolean

Check if a ticket is selected by ID

mandate

() => void

Mandates selected ID based on "mandatory" Option

first

() => void

Select the first Ticket in the collection

last

() => void

Select the last Ticket in the collection

next

() => void

Select the next Ticket based on current index

prev

() => void

Select the previous Ticket based on current index

step

(count: number) => void

Step through the collection by a given count

register

(ticket?: Partial<Z>) => E

Register a new ticket (accepts input type, returns output type)

onboard

(registrations: Partial<Z>[]) => E[]

Onboard multiple tickets at once

Was this page helpful?

© 2016-1970 Vuetify, LLC
Ctrl+/