Steps
Step are used to guide users through a series of steps in a process. It’s a great way to break down a complex process into smaller, more manageable step.
Features
Keyboard navigation
Give the step list an accessible name, for example by adding aria-label="Checkout steps"
to the element that receives getListProps().
Render one native button trigger for every index from zero through count - 1; keep
the selected trigger mounted and enabled. Per-trigger native disabling and partially
mounted step lists are not supported navigation modes. Use linear to gate activation.
Steps use manual tab activation. Tab enters the list at the selected step. Left and Right
move focus between horizontal steps, with the direction reversed in RTL layouts. Up and
Down move focus in vertical layouts. Navigation wraps at either end; Home and End focus
the first and last rendered, enabled triggers. Arrow keys do not change the current step
or call onStepChange. Press Enter or Space on the native trigger button to activate the
focused step. Tab leaves the list, and returning to it starts at the selected step again.
In linear mode, inactive triggers expose aria-disabled="true". Arrow keys can still
focus them so their labels and state remain discoverable, but activating them cannot
skip or revisit a step. Use the Next and Previous controls to move through the process.
In controlled mode, activation requests a change; the selected step and its content
change only after the parent accepts the new step.
The completion content at index: count is a terminal message, not a tab panel, because
it has no corresponding step trigger. Give it meaningful content or a heading. It remains
focusable, and the last real step is the list’s fallback entry point when focus is outside
the list. All steps are complete and none is selected in this terminal state. This state
is a process summary rather than the usual active-tab/active-panel pattern. If your
application hides the element containing focus when advancing, move focus to an
appropriate visible heading or control as part of that transition.
Install
Install the component from your command line.
Anatomy
Import all parts and piece them together.
<script setup lang="ts">import * as steps from '@destyler/steps'import { normalizeProps, useMachine } from '@destyler/vue'import { computed, useId } from 'vue'
const stepsData = [ { title: 'Step 1' }, { title: 'Step 2' }, { title: 'Step 3' },]
const [state, send] = useMachine( steps.machine({ id: useId(), count: stepsData.length, }),)
const api = computed(() => steps.connect(state.value, send, normalizeProps))</script>
<template> <div v-bind="api.getRootProps()"> <div v-bind="api.getListProps()"> <div v-for="(step, index) in stepsData" :key="index" v-bind="api.getItemProps({ index })" > <button v-bind="api.getTriggerProps({ index })"> <div v-bind="api.getIndicatorProps({ index })"></div> </button> <div v-bind="api.getSeparatorProps({ index })" /> </div> </div>
<div v-for="(step, index) in stepsData" :key="index" v-bind="api.getContentProps({ index })" > {{ step.title }} </div>
<div v-bind="api.getContentProps({ index: stepsData.length })"></div>
<button v-bind="api.getPrevTriggerProps()"></button> <button v-bind="api.getNextTriggerProps()"></button> </div></template>import { normalizeProps, useMachine } from '@destyler/react'import * as steps from '@destyler/steps'import { useId } from 'react'
const stepsData = [ { title: 'Step 1' }, { title: 'Step 2' }, { title: 'Step 3' },]
export default function Steps() { const [state, send] = useMachine( steps.machine({ id: useId(), count: stepsData.length, }), )
const api = steps.connect(state, send, normalizeProps)
return ( <div {...api.getRootProps()}> <div {...api.getListProps()}> {stepsData.map((step, index) => ( <div key={index} {...api.getItemProps({ index })}> <button {...api.getTriggerProps({ index })}> <div {...api.getIndicatorProps({ index })}></div> </button> <div {...api.getSeparatorProps({ index })}/> </div> ))} </div>
{stepsData.map((step, index) => ( <div key={index} {...api.getContentProps({ index })}></div> ))}
<div {...api.getContentProps({ index: stepsData.length })}></div>
<button {...api.getPrevTriggerProps()}></button> <button {...api.getNextTriggerProps()}></button> </div> )}<script lang="ts"> import * as steps from '@destyler/steps' import { normalizeProps, useMachine } from '@destyler/svelte'
const stepsData = [ { title: 'Step 1' }, { title: 'Step 2' }, { title: 'Step 3' }, ]
const [state, send] = useMachine( steps.machine({ id: crypto.randomUUID(), count: stepsData.length, }), )
const api = $derived(steps.connect(state, send, normalizeProps)) </script>
<div {...api.getRootProps()}> <div {...api.getListProps()}> {#each stepsData as step, index} <div {...api.getItemProps({ index })}> <button {...api.getTriggerProps({ index })}> <div {...api.getIndicatorProps({ index })} ></div> </button> <div {...api.getSeparatorProps({ index })}></div> </div> {/each} </div>
{#each stepsData as step, index} <div {...api.getContentProps({ index })}></div> {/each}
<div {...api.getContentProps({ index: stepsData.length })}></div>
<button {...api.getPrevTriggerProps()}></button> <button {...api.getNextTriggerProps()}></button> </div>import { normalizeProps, useMachine } from '@destyler/solid'import * as steps from '@destyler/steps'import { createMemo, createUniqueId } from 'solid-js'
const stepsData = [ { title: 'Step 1' }, { title: 'Step 2' }, { title: 'Step 3' },]
export default function Steps() { const [state, send] = useMachine( steps.machine({ id: createUniqueId(), count: stepsData.length, }), )
const api = createMemo(() => steps.connect(state, send, normalizeProps))
return ( <div {...api().getRootProps()} > <div {...api().getListProps()} > {stepsData.map((step, index) => ( <div {...api().getItemProps({ index })}> <button {...api().getTriggerProps({ index })} > <div {...api().getIndicatorProps({ index })}></div> </button> <div {...api().getSeparatorProps({ index })}/> </div> ))} </div>
{stepsData.map((step, index) => ( <div {...api().getContentProps({ index })}></div> ))}
<div {...api().getContentProps({ index: stepsData.length })} > </div>
<button {...api().getPrevTriggerProps()}></button> <button {...api().getNextTriggerProps()}></button> </div> )}Setting the initial step
Set the initial step by passing the step property to the machine context.
The value of the step property is zero-based index.
const [state, send] = useMachine( steps.machine({ defaultStep: 1, }),)Controlled usage
Steps step index can be uncontrolled or controlled. Prefer defaultStep for an uncontrolled seed. Passing step into machine(...) is presence-controlled — sync with onStepChange and adapter context. See Controllable State.
steps.machine({ id: "1", count: 3, defaultStep: 0,})
const step = 0 // your reactive statesteps.machine({ id: "1", count: 3, step, onStepChange(details) { // details => { step: number } },})Listening for step change
When the active step changes, the machine will invoke the onStepChange event
const [state, send] = useMachine( steps.machine({ onStepChange(details) { // details => { step: number } console.log(`changed to ${details.step}`) }, }),)Listening for steps completion
When all steps are completed, the machine will invoke the onStepComplete event
const [state, send] = useMachine( steps.machine({ onStepComplete() { console.log("All steps are complete") }, }),)Enforcing linear steps
To enforce linear steps, you can set the linear prop to true when creating the steps machine.
This will prevent users from skipping steps.
const [state, send] = useMachine( steps.machine({ linear: true, }),)Changing the orientation
The steps machine supports both horizontal and vertical orientations.
You can set the orientation prop to horizontal or vertical to change the orientation of the steps.
const [state, send] = useMachine( steps.machine({ orientation: "vertical", }),)Styling guide
Earlier, we mentioned that each splitter part has a
data-partattribute added to them to select and style them in the DOM.
[data-scope="steps"][data-part="root"] { /* styles for the root part */}
[data-scope="steps"][data-part="root"][data-orientation="horizontal"] { /* styles for the root part based on orientation */}
[data-scope="steps"][data-part="list"] { /* styles for the list part */}
[data-scope="steps"][data-part="list"][data-orientation="horizontal"] { /* styles for the list part based on orientation */}
[data-scope="steps"][data-part="separator"] { /* styles for the separator part */}
[data-scope="steps"][data-part="separator"][data-orientation="horizontal"] { /* styles for the separator part based on orientation */}Current Step
To style the current step, you can use the data-current attribute.
[data-scope="steps"][data-part="item"][data-current] { /* item styles for the current step */}
[data-scope="steps"][data-part="separator"][data-current] { /* separator styles for the current step */}Completed step
To style the completed step, you can use the data-complete attribute.
[data-scope="steps"][data-part="item"][data-complete] { /* item styles for the completed step */}
[data-scope="steps"][data-part="separator"][data-complete] { /* separator styles for the completed step */}Incomplete step
To style the incomplete step, you can use the data-incomplete attribute.
[data-scope="steps"][data-part="item"][data-incomplete] { /* item styles for the incomplete step */}
[data-scope="steps"][data-part="separator"][data-incomplete] { /* separator styles for the incomplete step */}Methods and Properties
Machine Context
The steps machine exposes the following context properties:
ElementIds number number (details: StepChangeDetails) => void VoidFunction boolean "horizontal" | "vertical" number "ltr" | "rtl" string () => ShadowRoot | Node | Document Machine API
The steps api exposes the following methods:
number number number boolean boolean boolean (step: number) => void () => void () => void () => void (props: ItemProps) => ItemState Data Attributes
Root
data-scope data-part data-orientation List
data-scope data-part data-orientation Item
data-scope data-part data-orientation data-complete data-current data-incomplete Trigger
data-scope data-part data-state data-orientation data-complete data-current data-incomplete Content
data-scope data-part data-state data-orientation Indicator
data-scope data-part data-complete data-current data-incomplete Separator
data-scope data-part data-orientation data-complete data-current data-incomplete Progress
data-scope data-part data-complete