Stepper
Overview
Displays progress through a multi-step workflow. SStepper combines a StepperRoot-family of Aria primitives (zero styles) with the stepperVariants style recipe (8 slots: root/item/trigger/indicator/indicatorIcon/itemContent/separator/title/description) and 7 colors × 7 sizes. In linear mode (the default) steps must be completed in order — future steps are disabled and cannot be skipped; in non-linear mode any step can be activated freely. The current step is announced to screen readers through a localized role="status" live region, and the group exposes an aria-label that falls back to the stepper.ariaLabel locale message.
Usage
Features
- 🔁 Linear mode by default —
linear(defaulttrue) disables inaccessible future steps and blocks jumping ahead; previous steps always remain reachable - 🧭 Horizontal / vertical —
orientationswitches the layout, connector direction and arrow-key navigation axis - 🎛️ Controlled / uncontrolled —
modelValue+update:modelValueordefaultValue(default1); the root slot exposesgoToStep/nextStep/prevStep/hasNext/hasPrev/totalStepsand more - 🖱️ Click, Enter or Space to select a step; arrow keys move focus (orientation-aware, honoring
dir), Home/End jump to the first/last reachable step - ♿ Accessible by default —
role="group"with a localizedaria-labelfallback,aria-current="step"on the active item,aria-labelledby/aria-describedbywiring between trigger/title/description, and arole="status"live region (stepper.stepOfmessage) - 🧩 Aria/styled split —
StepperCompactaggregates the 7 primitives and exposes 6*Propschannels (item/trigger/indicator/separator/title/description); thestepis always derived from item order - 🎨 8-slot styling — root/item/trigger/indicator/indicatorIcon/itemContent/separator/title/description with 7 colors (primary/destructive/success/warning/info/carbon/secondary/accent) and 7 sizes (xs–2xl)
- 🌐 Localized by default — the live region, group
aria-labeland indicator default content useuseLocaleMessages(stepper.step/stepper.ariaLabel/stepper.stepOf)
Component family
SStepper(styled) — the entry wrapper;stepperVariantsrecipe with dynamic 8-slot forwarding anduseForwardListenersevent merging; mirrors thelineardefault so an absent Boolean prop is not cast tofalseStepperRoot(Aria) — the state owner:useControllableStateovermodelValue/defaultValue,totalSteps/currentStepderivation,canGoToSteplinear gating, and thegoToStep/nextStep/prevStepAPI; rendersrole="group",data-linear,data-orientation, thearia-labelfallback and theVisuallyHiddenlive regionStepperItem(Aria) —step(1-based),disabled,completed; derivesdata-state(completed/active/inactive) from position andisFocusablefromlinear+disabledStepperTrigger(Aria) —buttonby default;mousedown.leftplus Enter/Space to select, arrow keys/Home/End to navigate; rendersdisabled/data-disabled/tabindex="-1"for unreachable steps and registers into the root collectionStepperIndicator(Aria) —spanby default; default content is the localizedstepper.stepmessage (Step {step})StepperTitle/StepperDescription(Aria) —h4/pdefaults; exposeids consumed by the trigger’saria-labelledby/aria-describedbyStepperSeparator(Aria) —SeparatorRoot-based connector;decorativeby default, orientation anddata-stateflow from root/item contextStepperCompact(Aria) — the aggregated composite; normalizes items withstep: index + 1, default indicator shows a check icon (lucide:checkvia the ConfigProvidericonRender) for completed steps or the step number otherwise, default title falls back toStep {n}; slots:item/indicator/title/description/separator
Demos
Basic
Vertical
Linear
Custom Styling
- 01 Basic — controlled
v-modeloveritems - 02 Vertical —
orientation="vertical"layout - 03 Linear — ordered completion with Previous/Next controls
- 04 Custom styling — 8-slot
uioverrides
API
Aria Layer
9Stepper
Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | ClassValue | — | Additional class names applied to the root element. |
color | ThemeColor | — | Theme color of the component. |
size | ThemeSize | — | Visual size of the component. |
ui | Partial<StepperUi> | — | Per-slot class overrides for the component. |
items | StepperItemData[] | — | Items rendered by the component. |
itemProps | Omit<StepperItemProps, 'step'> | — | Properties forwarded to the item element. The `step` is derived from the item order and cannot be overridden. |
triggerProps | StepperTriggerProps | — | Properties forwarded to the trigger element. |
indicatorProps | StepperIndicatorProps | — | Properties forwarded to the indicator element. |
separatorProps | SeparatorRootProps | — | Properties forwarded to the separator element. |
titleProps | StepperTitleProps | — | Properties forwarded to the title element. |
descriptionProps | StepperDescriptionProps | — | Properties forwarded to the description element. |
defaultValue | number | 1 | The value of the step that should be active when initially rendered. |
modelValue | number | — | The controlled value of the step to activate. Can be bind as `v-model`. |
orientation | DataOrientation | 'horizontal' | The orientation of the stepper. |
dir | Direction | — | The reading direction of the stepper. |
linear | boolean | true | Whether the steps must be completed in order. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | number | Emitted when the model value changes. |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
item | StepperItemData & { step: number; state: StepperState; } | Custom content for the item slot. |
indicator | StepperItemData & { step: number; state: StepperState; } | Custom content for the indicator slot. |
title | StepperItemData & { step: number; state: StepperState; } | Custom content for the title slot. |
description | StepperItemData & { step: number; state: StepperState; } | Custom content for the description slot. |
separator | StepperItemData & { step: number; state: StepperState; } | Custom content for the separator slot. |
StepperCompact
Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | StepperItemData[] | — | Items rendered by the component. |
itemProps | Omit<StepperItemProps, 'step'> | — | Properties forwarded to the item element. The `step` is derived from the item order and cannot be overridden. |
triggerProps | StepperTriggerProps | — | Properties forwarded to the trigger element. |
indicatorProps | StepperIndicatorProps | — | Properties forwarded to the indicator element. |
separatorProps | SeparatorRootProps | — | Properties forwarded to the separator element. |
titleProps | StepperTitleProps | — | Properties forwarded to the title element. |
descriptionProps | StepperDescriptionProps | — | Properties forwarded to the description element. |
defaultValue | number | 1 | The value of the step that should be active when initially rendered. |
modelValue | number | — | The controlled value of the step to activate. Can be bind as `v-model`. |
orientation | DataOrientation | 'horizontal' | The orientation of the stepper. |
dir | Direction | — | The reading direction of the stepper. |
linear | boolean | true | Whether the steps must be completed in order. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | number | Emitted when the model value changes. |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
item | StepperItemData & { step: number; state: StepperState; } | Custom content for the item slot. |
indicator | StepperItemData & { step: number; state: StepperState; } | Custom content for the indicator slot. |
title | StepperItemData & { step: number; state: StepperState; } | Custom content for the title slot. |
description | StepperItemData & { step: number; state: StepperState; } | Custom content for the description slot. |
separator | StepperItemData & { step: number; state: StepperState; } | Custom content for the separator slot. |
StepperDescription
Props
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
StepperIndicator
Props
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
StepperItem
Props
| Prop | Type | Default | Description |
|---|---|---|---|
step | number | — | The step index, starting from `1`. |
disabled | boolean | — | When `true`, prevents the user from interacting with the step. |
completed | boolean | — | Shows whether the step is completed. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
StepperRoot
Props
| Prop | Type | Default | Description |
|---|---|---|---|
defaultValue | number | 1 | The value of the step that should be active when initially rendered. |
modelValue | number | — | The controlled value of the step to activate. Can be bind as `v-model`. |
orientation | DataOrientation | 'horizontal' | The orientation of the stepper. |
dir | Direction | — | The reading direction of the stepper. |
linear | boolean | true | Whether the steps must be completed in order. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | number | Emitted when the model value changes. |
StepperSeparator
Props
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | DataOrientation | — | Orientation of the component.
Either `vertical` or `horizontal`. Defaults to `horizontal`. |
decorative | boolean | — | Whether or not the component is purely decorative. <br>When `true`, accessibility-related attributes are updated so
that that the rendered element is removed from the accessibility tree. |
StepperTitle
Props
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
StepperTrigger
Props
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
Notes
Architecture and benchmark differences
StepperRoot owns the whole state machine (controllable state, ordered item collection, linear gating) while every primitive stays style-free and only the UI wrapper injects the stepperVariants classes. isFocusable is derived per item (disabled || (!linear && step > currentStep + 1)) and mirrored into the DOM as disabled/data-disabled/tabindex, so unreachable steps are both non-clickable and out of the tab order. Arrow navigation uses useArrowNavigation with loop: false and respects orientation/dir; in linear mode focus cannot land on a disabled future step. The completed check icon requires a ConfigProvider iconRender (default: the Icon component renders nothing), otherwise completed steps fall back to their number. The live region and aria-label fallback are localized via useLocaleMessages, unlike shadcn/ui’s stepper block which hardcodes English strings.
| Capability | VeanUI | shadcn/ui (blocks) | Ant Design Steps | Element Plus Steps | Mantine Stepper |
|---|---|---|---|---|---|
| Aria/styled split | ✅ | — | — | — | — |
| Linear mode (ordered completion) | ✅ | ✅ | ⚠️ | ✅ | ✅ |
| Orientation (horizontal/vertical) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Arrow-key navigation (Home/End) | ✅ | — | — | — | — |
Localized live region + group aria-label | ✅ | ⚠️ | — | — | — |
| Controlled/uncontrolled | ✅ | — | ⚠️ | ⚠️ | ✅ |
| Compact composite with per-part props | ✅ | — | — | — | — |
completed/disabled per step | ✅ | ✅ | ✅ | ✅ | ✅ |
⚠️ = partial (AntD uses a status prop instead of a linear gate; Element Plus exposes process-status/finish-status without a navigation gate; shadcn/ui’s stepper block renders the status text with hardcoded English and is not a library component).
Cautions
steponStepperItemmust be 1-based and sequential:canGoToStepand the focus logic index the ordered collection bystep - 1. The compact normalizes this automatically from item order; custom Aria compositions must keep steps contiguous.itemPropsintentionally omitsstep— it is derived from item order and cannot be overridden.- In linear mode only the current step and the next one are reachable;
goToStep/nextStepsilently ignore unreachable targets, so drive navigation throughhasNext/hasPrevor thenextStep/prevStephelpers. - The live region (
role="status") only renders whentotalSteps > 0; give the root an explicitaria-labelwhen the default locale message is not appropriate. - The default indicator falls back to the step number when no ConfigProvider
iconRenderis present; provide one to render the completed check icon. disabledsteps are skipped by arrow navigation but the collection is still ordered; a fully disabled stepper should still expose the grouparia-label.
FAQ
How do I make future steps clickable?
Set linear="false" — every step becomes focusable and selectable regardless of the current position:
<SStepper :items="items" :linear="false" />How do I drive the stepper with buttons?
Listen to the root slot helpers or simply bind v-model and use hasNext/hasPrev from the root slot to gate your controls:
<SStepper v-model="value" :items="items" linear>
<template #default="{ hasNext, hasPrev, nextStep, prevStep }">
<!-- your custom layout with nextStep() / prevStep() -->
</template>
</SStepper>How do I render a vertical stepper?
Set orientation="vertical" — the connector and arrow-key navigation axis follow:
<SStepper :items="items" orientation="vertical" />How do I mark a step as completed regardless of position?
Pass completed in the item data; an explicit completed wins over position-derived state:
<SStepper :items="[{ title: 'Account', completed: true }, { title: 'Profile' }]" />How do I customize the indicator content?
Use the indicator slot — it receives the item data, step and state:
<SStepper :items="items">
<template #indicator="{ state, step }">
<span>{{ state === 'completed' ? '✓' : step }}</span>
</template>
</SStepper>How do I localize the status announcement?
The live region and group aria-label follow the ConfigProvider locale (stepper.stepOf / stepper.ariaLabel); override the label per-instance with aria-label:
<SStepper :items="items" aria-label="Checkout progress" />