Stepper ​

Shows where a person or a process is in an ordered sequence of steps.

Experimental — Stepper ships from frappe-ui/experimental while its API is reviewed and refined, so it is exempt from the usual deprecation policy and can change shape or disappear in any release.

ts
import { Stepper } from 'frappe-ui/experimental'
import type { StepItem, StepperProps } from 'frappe-ui/experimental'
  1. AccountName, email and passwordcompleted
  2. WorkspaceCompany name and URLcompleted
  3. Invite teamAdd people by emailskipped
  4. Import dataBring in leads from a CSVcurrent
  5. ReviewCheck everything and finishnot started
current
edge
size
<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.

  1. Invite teamAdd people by emailcurrent
  2. Import dataBring in leads from a CSVnot started
  3. ReviewCheck everything and finishnot started

Add people by email.

vue
<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.

  1. Accountcompleted
  2. Workspacecompleted
  3. Invite teamcurrent
  4. Import datanot started
  5. Reviewnot started
vue
<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.

  1. Resolution planningcompleted
  2. Scope alignmentcompleted
  3. Implementationrunning
    1. Configure solutioncompleted
    2. Connect systemscompleted
    3. Apply changesrunning
    4. Run workflownot started
    5. Prepare outputnot started
  4. Outcome verificationnot started
  5. Final summarynot started
vue
<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.

  1. Fetch sources0:42completed
  2. Install dependencies1:08completed
  3. Build assets0:31running
  4. Migrate databasenot started
  5. Deploynot started
vue
<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.

  1. completedAccount
  2. completedWorkspace
  3. skippedInvite team
  4. currentImport data
  5. not startedReview
vue
<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
typescript
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*
StepItem[]

Steps in order.

modelValue
= null
StepValue | null

Value of the current step; may be a sub-step's value.

completed
= false
boolean

Every step is done. Wins over `modelValue`.

loading
= false
boolean

The current step is running: its indicator's outline spins.

failed
= false
boolean

The current step failed.

vertical
= false
boolean

Lays the steps out as a vertical list instead of a segmented bar.

edge
= "start"
StepperEdge

Vertical only: the side the indicators sit on.

size
= "md"
StepperSize

Size of the indicators and text.

substeps
= "all"
"all" | "current"

Vertical only: show sub-steps for every step, or only the current one.

clickable
= false
boolean

Finished steps become buttons that set `v-model`. Off while `completed` is set, since `completed` would override the value a click sets.

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).

update:modelValue
[value: StepValue]

Fired when a finished step is clicked (`clickable` only).