Steps
Steps guide users through a multi-step process.
Features
- Supports horizontal and vertical orientations
- Supports changing the active step with the keyboard and pointer
- Supports linear and non-linear steps
Installation
Install the steps package:
npm install @zag-js/steps @zag-js/react # or yarn add @zag-js/steps @zag-js/react
npm install @zag-js/steps @zag-js/solid # or yarn add @zag-js/steps @zag-js/solid
npm install @zag-js/steps @zag-js/vue # or yarn add @zag-js/steps @zag-js/vue
npm install @zag-js/steps @zag-js/svelte # or yarn add @zag-js/steps @zag-js/svelte
Anatomy
Check the steps anatomy and part names.
Each part includes a
data-partattribute to help identify them in the DOM.
Usage
Import the steps package:
import * as steps from "@zag-js/steps"
The steps package exports two key functions:
machine- State machine logic.connect- Maps machine state to JSX props and event handlers.
Then use the framework integration helpers:
import * as steps from "@zag-js/steps" import { useMachine, normalizeProps } from "@zag-js/react" import { useId } from "react" const stepsData = [ { title: "Step 1" }, { title: "Step 2" }, { title: "Step 3" }, ] function Steps() { const service = useMachine(steps.machine, { id: useId(), count: stepsData.length, }) const api = steps.connect(service, 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 })}>{index + 1}</div> <span>{step.title}</span> </button> <div {...api.getSeparatorProps({ index })} /> </div> ))} </div> {stepsData.map((step, index) => ( <div key={index} {...api.getContentProps({ index })}> {step.title} </div> ))} <div {...api.getContentProps({ index: stepsData.length })}> Steps Complete - Thank you for filling out the form! </div> <div> <button {...api.getPrevTriggerProps()}>Back</button> <button {...api.getNextTriggerProps()}>Next</button> </div> </div> ) }
import * as steps from "@zag-js/steps" import { useMachine, normalizeProps } from "@zag-js/solid" import { createMemo, createUniqueId } from "solid-js" const stepsData = [ { title: "Step 1" }, { title: "Step 2" }, { title: "Step 3" }, ] function Steps() { const service = useMachine(steps.machine, { id: createUniqueId(), count: stepsData.length, }) const api = createMemo(() => steps.connect(service, normalizeProps)) return ( <div {...api().getRootProps()}> <div {...api().getListProps()}> <Index each={stepsData}> {(step, index) => ( <div {...api().getItemProps({ index })}> <button {...api().getTriggerProps({ index })}> <div {...api().getIndicatorProps({ index })}>{index + 1}</div> <span>{step().title}</span> </button> <div {...api().getSeparatorProps({ index })} /> </div> )} </Index> </div> <Index each={stepsData}> {(step, index) => ( <div {...api().getContentProps({ index })}> {step().title} - {step().description} </div> )} </Index> <div {...api().getContentProps({ index: stepsData.length })}> Steps Complete - Thank you for filling out the form! </div> <div> <button {...api().getPrevTriggerProps()}>Back</button> <button {...api().getNextTriggerProps()}>Next</button> </div> </div> ) }
<script setup> import * as steps from "@zag-js/steps" import { useMachine, normalizeProps } from "@zag-js/vue" import { computed } from "vue" const stepsData = [ { title: "Step 1" }, { title: "Step 2" }, { title: "Step 3" }, ] const service = useMachine(steps.machine, { id: "1", count: stepsData.length, }) const api = computed(() => steps.connect(service, 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 })">{{ index + 1 }}</div> <span>{{ step.title }}</span> </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 }} - {{ step.description }} </div> <div v-bind="api.getContentProps({ index: stepsData.length })"> Steps Complete - Thank you for filling out the form! </div> <div> <button v-bind="api.getPrevTriggerProps()">Back</button> <button v-bind="api.getNextTriggerProps()">Next</button> </div> </div> </template>
<script lang="ts"> import { normalizeProps, useMachine } from "@zag-js/svelte" import * as steps from "@zag-js/steps" const stepsData = [ { title: "Step 1" }, { title: "Step 2" }, { title: "Step 3" }, ] const service = useMachine(steps.machine, { id: "1", count: stepsData.length, }) const api = $derived(steps.connect(service, 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 })}>{index + 1}</div> <span>{step.title}</span> </button> <div {...api.getSeparatorProps({ index })} /> </div> {/each} </div> {#each stepsData as step, index} <div {...api.getContentProps({ index })}> {step.title} - {step.description} </div> {/each} <div {...api.getContentProps({ index: stepsData.length })}> Steps Complete - Thank you for filling out the form! </div> <div> <button {...api.getPrevTriggerProps()}>Back</button> <button {...api.getNextTriggerProps()}>Next</button> </div> </div>
Setting the initial step
Set the initial step by passing defaultStep to the machine context.
The value of the
stepproperty is zero-based index.
const service = useMachine(steps.machine, { defaultStep: 1, })
Controlled current step
Use step and onStepChange for controlled usage.
const service = useMachine(steps.machine, { step, onStepChange(details) { setStep(details.step) }, })
Listening for step change
When the active step changes, the machine will invoke the onStepChange event
const service = useMachine(steps.machine, { onStepChange(details) { // details => { step: number } console.log(`Step changed to ${details.step}`) }, })
Listening for steps completion
When all steps are completed, the machine will invoke the onStepComplete event
const service = 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 service = useMachine(steps.machine, { linear: true, })
Validating steps in linear mode
Use isStepValid to block forward navigation when a step is incomplete.
const service = useMachine(steps.machine, { linear: true, isStepValid(index) { return completedSteps.has(index) }, onStepInvalid(details) { // details => { step: number, action: "next" | "set", targetStep?: number } console.log("blocked at step", details.step) }, })
Skipping optional steps
Use isStepSkippable when some steps should be bypassed by next/prev
navigation.
const service = useMachine(steps.machine, { isStepSkippable(index) { return index === 2 }, })
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 service = useMachine(steps.machine, { orientation: "vertical", })
Programmatic navigation
Use the API methods to move through the flow in code.
api.setStep(2) api.goToNextStep() api.goToPrevStep() api.resetStep()
Styling guide
Each part includes a data-part attribute you can target in CSS.
[data-scope="steps"][data-part="root"] { /* styles for the root part */ } [data-scope="steps"][data-part="root"][data-orientation="horizontal|vertical"] { /* 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|vertical"] { /* 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|vertical"] { /* 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:
idsElementIdsThe custom ids for the stepper elementsstepnumberThe controlled value of the stepperdefaultStepnumberThe initial value of the stepper when rendered. Use when you don't need to control the value of the stepper.onStepChange(details: StepChangeDetails) => voidCallback to be called when the value changesonStepCompleteVoidFunctionCallback to be called when a step is completedlinearbooleanIf `true`, the stepper requires the user to complete the steps in orderorientation"horizontal" | "vertical"The orientation of the steppercountnumberThe total number of stepsisStepValid(index: number) => booleanWhether a step is valid. Invalid steps block forward navigation in linear mode.isStepSkippable(index: number) => booleanWhether a step can be skipped during navigation. Skippable steps are bypassed when using next/prev.onStepInvalid(details: StepInvalidDetails) => voidCalled when navigation is blocked due to an invalid step.dir"ltr" | "rtl"The document's text/writing direction.idstringThe unique identifier of the machine.getRootNode() => ShadowRoot | Node | DocumentA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The steps api exposes the following methods:
valuenumberThe value of the stepper.percentnumberThe percentage of the stepper.countnumberThe total number of steps.hasNextStepbooleanWhether the stepper has a next step.hasPrevStepbooleanWhether the stepper has a previous step.isCompletedbooleanWhether the stepper is completed.isStepValid(index: number) => booleanCheck if a specific step is valid (lazy evaluation)isStepSkippable(index: number) => booleanCheck if a specific step can be skippedsetStep(step: number) => voidFunction to set the value of the stepper.goToNextStepVoidFunctionFunction to go to the next step.goToPrevStepVoidFunctionFunction to go to the previous step.resetStepVoidFunctionFunction to go to reset the stepper.getItemState(props: ItemProps) => ItemStateReturns the state of the item at the given index.