Skip to content
ShineUI
Install

Stepper

Progress with numbered feet.

<sui-stepper>PanelsStable

About

Wizard progress: numbered circles joined by rules. Completed steps are brand with a check, the current step takes the accent, the rest stay outlined. Horizontal by default; orientation="vertical" stacks the steps. Reach for it when a task runs in a fixed order and the reader needs to see where they are. Narrow, the rail folds to circles and a caption.

Usage

<sui-stepper active="0"> <div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Choose the bank that holds the operating account. We support First Harrison direct feeds.</div> </sui-stepper> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-stepper"); el.steps = [ { label: "Connect bank", description: "First Harrison" }, { label: "Map accounts", description: "4 of 12 mapped" }, { label: "Review", description: "Aug 1 to Aug 31" }, { label: "Finish" }, ]; /* SuiStep[] */ el.addEventListener("suiActiveChange", (e) => console.log(e.detail)); </script>
import { SuiStepper } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const steps = [ { label: "Connect bank", description: "First Harrison" }, { label: "Map accounts", description: "4 of 12 mapped" }, { label: "Review", description: "Aug 1 to Aug 31" }, { label: "Finish" }, ]; /* SuiStep[] */ return ( <SuiStepper active="0" steps={steps} onSuiActiveChange={(e) => console.log(e.detail)}> <div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Choose the bank that holds the operating account. We support First Harrison direct feeds.</div> </SuiStepper> ); }
<script setup> import { SuiStepper } from "@shineui/vue"; import "@shineui/core/shineui.css"; const steps = [ { label: "Connect bank", description: "First Harrison" }, { label: "Map accounts", description: "4 of 12 mapped" }, { label: "Review", description: "Aug 1 to Aug 31" }, { label: "Finish" }, ]; // SuiStep[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiStepper active="0" :steps="steps" @sui-active-change="onChange"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Choose the bank that holds the operating account. We support First Harrison direct feeds.</div></SuiStepper> </template>
import { Component } from "@angular/core"; import { SuiStepper } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiStepper], template: `<sui-stepper [(active)]="active" [steps]="steps"><div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Choose the bank that holds the operating account. We support First Harrison direct feeds.</div></sui-stepper>`, }) export class Example { active = 0; steps = [ { label: "Connect bank", description: "First Harrison" }, { label: "Map accounts", description: "4 of 12 mapped" }, { label: "Review", description: "Aug 1 to Aug 31" }, { label: "Finish" }, ]; // SuiStep[] }

Basic

Completed steps go navy with a check, the current step is orange, the rest stay outlined. The default slot is the body of the current step.

Choose the bank that holds the operating account. We support First Harrison direct feeds.
<sui-stepper id="stp-basic" active="0"> <div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Choose the bank that holds the operating account. We support First Harrison direct feeds.</div> </sui-stepper>
// setup(root): properties and events setup(root) { root.querySelector('#stp-basic').steps = STEPS; }

Vertical

orientation="vertical" stacks the steps and moves the body under the current label, which suits a narrow column.

Match each bank account to a GL account. Unmapped accounts post to 9999 Suspense.
<sui-stepper id="stp-vertical" orientation="vertical" active="1" style="max-width:340px"> <div style="font-size:13px;color:var(--ink-muted);line-height:1.7">Match each bank account to a GL account. Unmapped accounts post to 9999 Suspense.</div> </sui-stepper>
// setup(root): properties and events setup(root) { root.querySelector('#stp-vertical').steps = STEPS; }

Linear and read-only

linear opens steps in order only: completed steps stay reachable and the ones ahead are refused, so the user can go back but not skip. readonly makes the whole rail a progress display with no buttons.

<div style="display:flex;flex-direction:column;gap:18px"> <sui-stepper id="stp-linear" linear active="1"></sui-stepper> <sui-stepper id="stp-static" readonly active="2"></sui-stepper> </div>
// setup(root): properties and events setup(root) { root.querySelector('#stp-linear').steps = STEPS; root.querySelector('#stp-static').steps = STEPS; }

Advancing the wizard

Picking a reachable step emits suiActiveChange with the new index, while writing active from script emits nothing, so the host advances with its own Next button and validates first. The current step carries aria-current="step", reachable steps are buttons, and gated steps are plain text.

Choose the bank that holds the operating account.
Step 1 of 4.
<div style="display:flex;flex-direction:column;gap:12px"> <sui-stepper id="stp-events" linear active="0"> <div id="stp-body" style="font-size:13px;color:var(--ink-muted);line-height:1.7">Choose the bank that holds the operating account.</div> </sui-stepper> <div style="display:flex;gap:8px;align-items:center"> <sui-button id="stp-back" variant="outline" size="sm" label="Back"></sui-button> <sui-button id="stp-next" variant="accent" size="sm" label="Next"></sui-button> <span id="stp-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Step 1 of 4.</span> </div> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#stp-events'); const body = root.querySelector('#stp-body'); const log = root.querySelector('#stp-log'); const copy = [ 'Choose the bank that holds the operating account.', 'Match each bank account to a GL account.', 'Check the August entries before they post.', 'The feed is live. New transactions arrive nightly.', ]; el.steps = STEPS; const show = (i, how) => { body.textContent = copy[i]; log.textContent = `Step ${i + 1} of ${STEPS.length}. ${how}`; }; el.addEventListener('suiActiveChange', (e) => show(e.detail, 'Picked on the rail, suiActiveChange fired.')); root.querySelector('#stp-next').addEventListener('click', () => { if (el.active >= STEPS.length - 1) return; el.active += 1; show(el.active, 'Advanced by the host, no event.'); }); root.querySelector('#stp-back').addEventListener('click', () => { if (el.active <= 0) return; el.active -= 1; show(el.active, 'Moved back by the host, no event.'); }); }

Keyboard

KeyAction
TabMoves through the reachable steps; each is a tab stop, and a gated step takes no focus.
Enter SpacePicks the focused step and emits suiActiveChange.

API

<sui-stepper>

Properties

PropertyAttributeTypeDefaultDescription
activeactivenumber0Index of the current step. Seeds it (the reference defaultActive) and tracks it afterwards; writing it from script emits nothing.
linearlinearbooleanfalseSteps open in order only: a step ahead of active is not a button and cannot be picked, so the user can go back but not skip forward. Advancing is the host's job (its own Next button sets active), which is what lets it validate the step first. Reflected.
orientationorientation"horizontal" | "vertical"'horizontal'Rail direction. vertical stacks the steps with the body under the current one's label. Reflected, as on every other element that carries it.
readOnlyreadonlybooleanfalseA read-only progress rail: no step is a button, so the reader sees where the task stands and cannot move it. Without it every reachable step is a button that sets active and emits suiActiveChange. The current step carries aria-current="step" either way. Reflected.
stepsproperty onlySuiStep[][]Steps in order ({ label, description? }). Property, not attribute.

Events

EventDetailReactVueDescription
suiActiveChangenumberonSuiActiveChange@sui-active-changeFires when a step is picked; detail is the new active index. Does not bubble.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves keyboard focus to the current step when it is a button, else the first step that is. Does nothing on a read-only rail, which has no tab stop.

Slots

SlotDescription
(default)Body of the current step (below the rail, or under the current label when vertical).

Types

SuiStep interfaceOne wizard step.
labelstring
description?string

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also