Stepper
Shows where a person or a process is in an ordered sequence of steps.
Experimental —
Stepperships fromfrappe-ui/experimentalwhile its API is reviewed and refined, so it is exempt from the usual deprecation policy and can change shape or disappear in any release.
import { Stepper } from 'frappe-ui/experimental'
import type { StepItem, StepperProps } from 'frappe-ui/experimental'- AccountName, email and passwordcompleted
- WorkspaceCompany name and URLcompleted
- Invite teamAdd people by emailskipped
- Import dataBring in leads from a CSVcurrent
- ReviewCheck everything and finishnot started
<Stepper
:steps="steps"
model-value="import"
vertical
/>Examples
Setup wizard
clickable turns finished steps into buttons, so a person can go back. A step marked skipped shows as passed, without a check.
<script setup lang="ts">
// A setup wizard: finished steps are clickable, and a step can be skipped.
import { computed, ref } from 'vue'
import { Button } from 'frappe-ui'
import { Stepper } from '..'
import type { StepItem } from '..'
const steps = ref<StepItem[]>([
{
value: 'account',
label: 'Account',
description: 'Name, email and password',
},
{
value: 'workspace',
label: 'Workspace',
description: 'Company name and URL',
},
{ value: 'invite', label: 'Invite team', description: 'Add people by email' },
{
value: 'import',
label: 'Import data',
description: 'Bring in leads from a CSV',
},
{
value: 'review',
label: 'Review',
description: 'Check everything and finish',
},
])
const current = ref<string | number>('invite')
const index = computed(() =>
steps.value.findIndex((s) => s.value === current.value),
)
const isLast = computed(() => index.value === steps.value.length - 1)
function next(skip = false) {
steps.value[index.value].skipped = skip
if (!isLast.value) current.value = steps.value[index.value + 1].value
}
</script>
<template>
<div class="flex w-full max-w-lg gap-8">
<div class="w-56 shrink-0">
<Stepper v-model="current" :steps="steps" vertical clickable />
</div>
<div class="flex flex-1 flex-col justify-between gap-4">
<p class="text-p-base text-ink-gray-7">{{ steps[index].description }}.</p>
<div class="flex justify-end gap-2">
<Button v-if="!isLast" variant="ghost" @click="next(true)">Skip</Button>
<Button variant="solid" :disabled="isLast" @click="next()"
>Continue</Button
>
</div>
</div>
</div>
</template>Plain
vertical with nothing else: the step number sits inside every step that is not finished yet.
<script setup lang="ts">
// The default vertical list with nothing extra: numbers and labels.
import { Stepper } from '..'
const steps = [
{ value: 'account', label: 'Account' },
{ value: 'workspace', label: 'Workspace' },
{ value: 'invite', label: 'Invite team' },
{ value: 'import', label: 'Import data' },
{ value: 'review', label: 'Review' },
]
</script>
<template>
<div class="w-56">
<Stepper :steps="steps" model-value="invite" vertical />
</div>
</template>Run progress with sub-steps
A step with children gets its own line of sub-steps. loading spins the outline of the current step's circle; failed marks it failed. edge="end" puts the indicators after the labels.
<script setup lang="ts">
// A long-running job whose third stage has parts: the one nested example.
import { onBeforeUnmount, ref } from 'vue'
import { Button } from 'frappe-ui'
import { Stepper } from '..'
import type { StepItem } from '..'
const steps: StepItem[] = [
{ value: 'plan', label: 'Resolution planning' },
{ value: 'scope', label: 'Scope alignment' },
{
value: 'implement',
label: 'Implementation',
children: [
{ value: 'configure', label: 'Configure solution' },
{ value: 'connect', label: 'Connect systems' },
{ value: 'apply', label: 'Apply changes' },
{ value: 'run', label: 'Run workflow' },
{ value: 'output', label: 'Prepare output' },
],
},
{ value: 'verify', label: 'Outcome verification' },
{ value: 'summary', label: 'Final summary' },
]
const order = steps.flatMap((s) => s.children ?? [s]).map((s) => s.value)
const current = ref<string | number>('apply')
const completed = ref(false)
const failed = ref(false)
let timer: ReturnType<typeof setInterval> | undefined
function advance() {
const i = order.indexOf(current.value)
if (i < order.length - 1) current.value = order[i + 1]
else completed.value = true
}
function run() {
clearInterval(timer)
current.value = order[0]
completed.value = false
failed.value = false
timer = setInterval(
() => (completed.value ? clearInterval(timer) : advance()),
900,
)
}
onBeforeUnmount(() => clearInterval(timer))
</script>
<template>
<div class="flex flex-col items-center gap-6">
<div class="w-72">
<Stepper
v-model="current"
:steps="steps"
:completed="completed"
:failed="failed"
:loading="!completed && !failed"
vertical
edge="end"
/>
</div>
<div class="flex gap-2">
<Button @click="run">Run again</Button>
<Button :disabled="completed" @click="failed = !failed">
{{ failed ? 'Retry' : 'Fail this step' }}
</Button>
</div>
</div>
</template>Compact
size="sm" for a sidebar or a run log. meta shows trailing text such as a duration.
<script setup lang="ts">
// The small size for a sidebar or run log, with each step's duration.
import { Stepper } from '..'
const steps = [
{ value: 'fetch', label: 'Fetch sources', meta: '0:42' },
{ value: 'install', label: 'Install dependencies', meta: '1:08' },
{ value: 'build', label: 'Build assets', meta: '0:31' },
{ value: 'migrate', label: 'Migrate database' },
{ value: 'deploy', label: 'Deploy' },
]
</script>
<template>
<div class="w-64">
<Stepper :steps="steps" model-value="build" size="sm" vertical loading />
</div>
</template>Segmented
Without vertical, the stepper is the stock Progress with one interval per step, and the labels underneath. Sub-steps are not shown.
<script setup lang="ts">
// The horizontal default: the stock Progress with one interval per step.
import { Stepper } from '..'
const steps = [
{ value: 'account', label: 'Account' },
{ value: 'workspace', label: 'Workspace' },
{ value: 'invite', label: 'Invite team', skipped: true },
{ value: 'import', label: 'Import data' },
{ value: 'review', label: 'Review' },
]
</script>
<template>
<div class="w-full max-w-2xl">
<Stepper :steps="steps" model-value="import" />
</div>
</template>Behavior
Current step
v-model holds the current step's value, which can be a sub-step's value. Every step before it shows as done and every step after it as upcoming. An empty value, or one that matches no step, shows every step as upcoming. completed shows every step as done.
A step with sub-steps follows them: done once every sub-step is done or skipped, current while it holds the current sub-step. Naming the parent itself makes its first sub-step current.
Skipped steps
Set skipped on a step the flow passed without doing. It shows as skipped once the current step is past it, and as upcoming before that. The line through a skipped step fills like a finished one.
Running and failed
loading and failed describe the current step. loading keeps the circle and its number still and moves a short arc around the outline. It stops under reduced motion.
Going back
With clickable, finished and skipped steps are buttons. Clicking one emits update:modelValue with its value. While completed is set, no step is a button, because completed overrides the value a click would set. The stepper never moves on its own: the caller decides what the next step is.
Accessibility
The steps are an ordered list, and sub-steps a nested list. The current step has aria-current="step". Each indicator carries a hidden word for its state ("completed", "running", "skipped", …), so the list reads without the icons. With clickable, finished steps are buttons you can reach with Tab and press with Enter or Space.
API Reference
Show types
export type StepValue = string | number
/** A step's state, derived from its position relative to the current step. */
export type StepState =
| 'complete'
| 'current'
| 'upcoming'
| 'failed'
| 'skipped'
export type StepperSize = 'sm' | 'md'
export type StepperEdge = 'start' | 'end'
/** A step to render. Holds data only; the stepper never writes to it. */
export interface StepItem {
/** Unique across the whole tree, sub-steps included. */
value: StepValue
/** Text shown for the step. */
label: string
/** Second line under the label. */
description?: string
/** Trailing text, such as a duration. */
meta?: string
/** The flow passed this step without doing it. */
skipped?: boolean
/** One level of sub-steps. Deeper nesting is ignored. */
children?: StepItem[]
}
export interface StepperProps {
/** Steps in order. */
steps: StepItem[]
/** Value of the current step; may be a sub-step's value. */
modelValue?: StepValue | null
/** Every step is done. Wins over `modelValue`. */
completed?: boolean
/** The current step is running: its indicator's outline spins. */
loading?: boolean
/** The current step failed. */
failed?: boolean
/** Lays the steps out as a vertical list instead of a segmented bar. */
vertical?: boolean
/** Vertical only: the side the indicators sit on. */
edge?: StepperEdge
/** Size of the indicators and text. */
size?: StepperSize
/** Vertical only: show sub-steps for every step, or only the current one. */
substeps?: 'all' | 'current'
/**
* Finished steps become buttons that set `v-model`. Off while `completed`
* is set, since `completed` would override the value a click sets.
*/
clickable?: boolean
}
export interface StepperEmits {
/** Fired when a finished step is clicked (`clickable` only). */
'update:modelValue': [value: StepValue]
}
/** Props every `#step-*` slot receives. */
export interface StepSlotProps {
item: StepItem
/** Position among its siblings. */
index: number
state: StepState
}
/** A step with its derived state, as rendered. */
export interface ResolvedStep {
item: StepItem
index: number
state: StepState
children: ResolvedStep[]
}Steps in order.
Value of the current step; may be a sub-step's value.
Every step is done. Wins over `modelValue`.
The current step is running: its indicator's outline spins.
The current step failed.
Lays the steps out as a vertical list instead of a segmented bar.
Vertical only: the side the indicators sit on.
Size of the indicators and text.
Vertical only: show sub-steps for every step, or only the current one.
Finished steps become buttons that set `v-model`. Off while `completed` is set, since `completed` would override the value a click sets.
| Slot | Payload |
|---|---|
step-prefix | StepSlotProps Replaces a step's indicator. |
step-label | StepSlotProps Replaces a step's label. |
step-description | StepSlotProps Replaces a step's description (vertical only). |
step-suffix | StepSlotProps Replaces a step's trailing meta text (vertical only). |
Replaces a step's indicator.
Replaces a step's label.
Replaces a step's description (vertical only).
Replaces a step's trailing meta text (vertical only).
| Event | Payload |
|---|---|
update:modelValue | [value: StepValue] Fired when a finished step is clicked (`clickable` only). |
Fired when a finished step is clicked (`clickable` only).