Skip to content

Stepper

NavigationSince 0.3.0
import { Stepper } from '@absoluteui/components/stepper';
import '@absoluteui/components/stepper/style.css';

Using Angular? See Stepper for Angular.

Show progress through a multi-step flow and let users move between steps. Stepper renders navigation: a labelled <nav> with an ordered list of steps. Reachable steps are links (when they have an href) or buttons; the current step carries aria-current="step"; unreachable steps are text. In linear mode, a step after the current one is reachable only when every step before it is complete. Activation emits a cancelable aui-navigate, so the application can guard the move, and then aui-change, so it can switch its own panels. Step status (complete, error) is data owned by the application; the stepper never infers completion.

  • Multi-step flows with a known, short sequence of steps, such as checkout, onboarding, or a setup wizard.
  • Showing which steps are complete, which step is current, and which are still ahead.
  • Letting users return to earlier steps (and, in non-linear flows, jump ahead).
  • Switching between peer views that have no order: use Tabs.
  • Showing where a page sits in a hierarchy: use Breadcrumb.
  • Reporting progress of a single task: use Progress Bar.
  • Flows with more than about 7 steps or with branching paths: split the flow or use a list of sections.

Checkout progress with a completed first step

Section titled “Checkout progress with a completed first step”
import { Stepper } from '@absoluteui/components/stepper';
const host = document.createElement('div');
document.body.append(host);
Stepper.create(host, {
label: 'Checkout progress',
steps: [
{ id: 'cart', label: 'Cart', status: 'complete' },
{ id: 'shipping', label: 'Shipping' },
{ id: 'payment', label: 'Payment' },
{ id: 'review', label: 'Review', optional: true },
],
value: 'shipping',
});

Linear wizard: guard forward moves and unlock steps on completion

Section titled “Linear wizard: guard forward moves and unlock steps on completion”
import { Stepper, type StepperChangeDetail, type StepperNavigateDetail, type StepperStep } from '@absoluteui/components/stepper';
const host = document.querySelector<HTMLElement>('#signup-steps');
const form = document.querySelector<HTMLFormElement>('#signup-form');
const next = document.querySelector<HTMLButtonElement>('#signup-next');
if (host && form && next) {
let steps: StepperStep[] = [
{ id: 'account', label: 'Account' },
{ id: 'profile', label: 'Profile', description: 'Name and photo' },
{ id: 'finish', label: 'Finish' },
];
const stepper = Stepper.create(host, { label: 'Sign-up steps', steps });
form.dataset.step = stepper.value;
// Guard forward moves: the current section must be valid first.
host.addEventListener('aui-navigate', (event) => {
const { index, previous } = (event as CustomEvent<StepperNavigateDetail>).detail;
const from = steps.findIndex((s) => s.id === previous);
if (index > from && !form.reportValidity()) event.preventDefault();
});
host.addEventListener('aui-change', (event) => {
form.dataset.step = (event as CustomEvent<StepperChangeDetail>).detail.value;
});
// "Next": mark the current step complete (unlocking the next one) and move on.
next.addEventListener('click', () => {
if (!form.reportValidity()) return;
const index = steps.findIndex((s) => s.id === stepper.value);
steps = steps.map((s, i): StepperStep => (i === index ? { ...s, status: 'complete' } : s));
const following = steps[index + 1];
stepper.update({ steps, ...(following ? { value: following.id } : {}) });
form.dataset.step = stepper.value;
});
}
Section titled “Non-linear vertical stepper with link steps and an error step”
import { Stepper } from '@absoluteui/components/stepper';
const host = document.querySelector<HTMLElement>('#application-steps');
if (host) {
const stepper = Stepper.create(host, {
label: 'Application sections',
orientation: 'vertical',
linear: false,
steps: [
{ id: 'personal', label: 'Personal details', href: '/apply/personal', status: 'complete' },
{ id: 'income', label: 'Income', href: '/apply/income', status: 'error', description: '2 fields need attention' },
{ id: 'documents', label: 'Documents', href: '/apply/documents' },
{ id: 'submit', label: 'Submit', href: '/apply/submit', disabled: true },
],
value: 'documents',
});
host.dataset.canSubmit = String(stepper.isReachable('submit'));
}
import { Stepper, type StepperChangeDetail, type StepperNavigateDetail } from '@absoluteui/components/stepper';
import '@absoluteui/components/stepper/style.css';
const host = document.querySelector<HTMLElement>('#checkout-steps');
const form = document.querySelector<HTMLFormElement>('#checkout');
if (host && form) {
const steps = [
{ id: 'shipping', label: 'Shipping' },
{ id: 'payment', label: 'Payment' },
{ id: 'review', label: 'Review' },
];
Stepper.create(host, { label: 'Checkout progress', steps, linear: false });
host.addEventListener('aui-navigate', (event) => {
const { index, previous } = (event as CustomEvent<StepperNavigateDetail>).detail;
const from = steps.findIndex((s) => s.id === previous);
if (index > from && !form.reportValidity()) event.preventDefault();
});
host.addEventListener('aui-change', (event) => {
form.dataset.step = (event as CustomEvent<StepperChangeDetail>).detail.value;
});
}
import { Stepper } from '@absoluteui/components/stepper';
const host = document.createElement('div');
document.body.append(host);
Stepper.create(host, {
label: 'Checkout progress',
steps: [
{ id: 'cart', label: 'Cart', status: 'complete' },
{ id: 'shipping', label: 'Shipping' },
{ id: 'payment', label: 'Payment' },
],
value: 'shipping',
});
<div class="aui-stepper aui-stepper--horizontal"> (consumer host; inline-size container)
<nav class="aui-stepper__nav" aria-label="Checkout progress">
<ol class="aui-stepper__list">
<li class="aui-stepper__step" data-state="complete" data-reachable="true">
<button type="button" class="aui-stepper__control" data-index="0"> (<a href> when the step has an href)
<span class="aui-stepper__indicator" aria-hidden="true"><svg class="aui-icon">...</svg></span>
<span class="aui-stepper__text">
<span class="aui-stepper__label">Cart</span>
<span class="aui-stepper__optional">Optional</span> (optional: true)
<span class="aui-stepper__description">...</span> (description)
</span>
<span class="aui-stepper__status">Completed</span> (visually hidden; "Error" for errors)
</button>
</li>
<li class="aui-stepper__step" data-state="current">
<span class="aui-stepper__control" aria-current="step">...<span class="aui-stepper__indicator" aria-hidden="true">2</span>...</span>
</li>
<li class="aui-stepper__step" data-state="upcoming">
<span class="aui-stepper__control">...</span> (unreachable: text)
</li>
</ol>
</nav>
</div>

The connector line between steps is a decorative ::after on .aui-stepper__step.

Stepper.create(host: HTMLElement, options: StepperOptions): Stepper

Global configuration key: stepper (see Configuration).

Option Type Default Description
label string Accessible name of the <nav> (aria-label), e.g. “Checkout progress”. Must not be empty. Required.
steps readonly { id: string; label: string; description?: string; href?: string; status?: 'complete' | 'error'; optional?: boolean; disabled?: boolean }[] Steps in order (at least one). Ids must be unique and non-empty; labels must be non-empty. status is consumer-owned; href renders a reachable step as a link. Required.
value string "id of the first step" Current step id. Must be the id of an existing step.
orientation 'horizontal' | 'vertical' "horizontal" Layout. A horizontal stepper renders vertically when its container is 30rem wide or narrower. Globally configurable.
linear boolean true When true, a step after the current one is reachable only when every step before it has status ‘complete’. Earlier steps are always reachable unless disabled. Globally configurable.
direction 'ltr' | 'rtl' "ltr" ‘rtl’ sets dir=“rtl” on the <nav>; ‘ltr’ leaves the document direction in effect. Globally configurable. Inherited from common configuration.
icons Partial<{ complete: IconRef; error: IconRef }> Icon slot overrides for the step indicator.
Property Type Description
host HTMLElement Consumer host; its children are owned by the component.
nav HTMLElement The <nav> landmark.
value string Current step id.
options Readonly<StepperOptions> Current instance options.
Method Description
state(id: string): StepperStepState | null Resolved state (‘current’ | ‘complete’ | ‘error’ | ‘upcoming’) of a step, or null for an unknown id.
isReachable(id: string): boolean Whether the user can activate the step in the current state. False for the current step, disabled steps, and unknown ids.
select(id: string): void Make a step current programmatically. No events and no reachability check. Throws STEPPER_OPTIONS for an unknown id.
update(options: Partial<StepperOptions>): void Merge, validate, and apply options. When the current id no longer exists and no value is given, the first step becomes current. No events.
destroy(): void Remove rendering, host classes, listeners, and the configuration subscription.
Event Detail Bubbles Cancelable Description
aui-navigate { id: string; index: number; step: StepperStep; previous: string } Yes Yes The user activated a reachable step (unmodified primary click, Enter, or Space on a button). preventDefault keeps the current step and stops link navigation.
aui-change { value: string; previous: string } Yes No The current step changed after an uncanceled aui-navigate.

All rules live in the aui.components cascade layer, so application styles override them without specificity battles.

Class Purpose
aui-stepper Host; an inline-size container.
aui-stepper--horizontal | --vertical Orientation modifiers on the host (the configured orientation).
aui-stepper__nav The <nav> landmark.
aui-stepper__list The <ol>.
aui-stepper__step An <li>; data-state=current|complete|error|upcoming, data-reachable=true, data-disabled=true.
aui-stepper__control <a>, <button>, or <span> for the step.
aui-stepper__indicator Decorative number or icon circle (aria-hidden).
aui-stepper__text | __label | __optional | __description Step text parts.
aui-stepper__status Visually hidden status text (“Completed”, “Error”).
Slot Default icon
complete check
error alert-circle

Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.

--aui-border-width-strong | --aui-color-border-strong | --aui-color-error-fg | --aui-color-focus-ring | --aui-color-on-primary | --aui-color-primary | --aui-color-surface | --aui-color-text | --aui-color-text-disabled | --aui-color-text-muted | --aui-control-height-sm | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-family-sans | --aui-font-size-md | --aui-font-size-sm | --aui-font-weight-semibold | --aui-icon-size-sm | --aui-line-height-tight | --aui-radius-pill | --aui-radius-sm | --aui-space-1 | --aui-space-2 | --aui-space-4

type StepperOrientation = 'horizontal' | 'vertical';
type StepperStepStatus = 'complete' | 'error';
type StepperStepState = 'complete' | 'error' | 'current' | 'upcoming';
interface StepperConfig { orientation: StepperOrientation; linear: boolean }
interface StepperStep { id: string; label: string; description?: string; href?: string; status?: StepperStepStatus; optional?: boolean; disabled?: boolean }
interface StepperIcons { complete: IconRef; error: IconRef }
interface StepperOptions extends Partial<StepperConfig> { label: string; steps: readonly StepperStep[]; value?: string; direction?: Direction; icons?: Partial<StepperIcons> }
interface StepperNavigateDetail { id: string; index: number; step: StepperStep; previous: string }
interface StepperChangeDetail { value: string; previous: string }

Pattern: Navigation landmark with an ordered list of steps (progress indicator). Reachable steps are native links or buttons; the current step is marked with aria-current=“step”. Not a tablist.

Key Context Action
Tab / Shift+Tab Any Moves between reachable steps (native tab order). The current step and unreachable steps are not Tab stops.
Enter Reachable step focused Emits cancelable aui-navigate; unless prevented, the step becomes current and aui-change is emitted. Links then navigate natively.
Space Reachable button step focused Same as Enter (native button activation).
Element Role
<nav> navigation
<ol> list
<li> listitem
reachable step with href link (native <a href>)
reachable step without href button (native <button type=“button”>)
current or unreachable step none (text <span>)
Attribute Element Value
aria-label <nav> label option
aria-current current step control step
aria-hidden step indicator (number or icon) true
dir <nav> rtl when direction is ‘rtl’; absent otherwise

Each reachable step is one Tab stop. After a button step is activated, focus moves to the new current step (tabindex=-1), so focus is not lost when the activated control is re-rendered as text. After a link step is activated, focus is not moved. update() and configuration changes rebuild the list and do not restore focus.

Criterion Level Note
1.3.1 Info and Relationships A
1.4.1 Use of Color A
2.1.1 Keyboard A
2.4.3 Focus Order A
2.4.7 Focus Visible AA
4.1.2 Name, Role, Value A

Automated test results: Accessibility and compliance.

  • Step status is conveyed as text: a visually hidden “Completed” or “Error” inside the step control and a visible “Optional” hint, not by color or icon alone.
  • The ordered list conveys the number of steps and each step’s position.
  • Disabled and unreachable steps are plain text, so they are announced but not operable.
  • Manual screen-reader verification (NVDA, JAWS, VoiceOver) is pending.
  • Unreachable steps are not exposed as disabled controls; users hear them as text without an explanation of why they cannot be activated.

Step states: upcoming, current, complete, error. Modifiers: optional, disabled, reachable, unreachable. Interaction: hover (label underline on reachable steps), focus-visible. Layout: horizontal, vertical.

From Trigger To
step A current reachable step B activated aui-navigate; unless prevented, B current and aui-change
step A current aui-navigate prevented A stays current; a link step does not navigate
any select(id) id current, no events
any update({ steps }) with new statuses reachability recomputed
any update({ steps }) without the current id first step current, no events
horizontal container 30rem or narrower vertical layout (class unchanged)
  • Status is never conveyed by color or icon alone (WCAG 1.4.1): hidden status text plus a visible “Optional” hint.
  • After a button step is activated, focus moves to the new current step (tabindex="-1"), because the activated button is re-rendered as text.
  • Unreachable steps are text without an explanation; describe the rule near the stepper (for example “Complete this step to continue”) when it is not obvious.
  • Manual screen-reader verification is pending (see Known limitations).

The host is a size container. A horizontal stepper uses the vertical layout when its own container is 30rem wide or narrower (@container (max-width: 30rem)), independent of the viewport. Labels and descriptions wrap and break anywhere. Horizontal steps share the width equally.

Logical properties throughout: horizontal steps and connectors follow the inline direction, and the vertical connector runs along the inline-start edge. direction: 'rtl' sets dir="rtl" on the <nav>; with 'ltr' (default) no dir is set, so an ancestor dir="rtl" also applies. Icons are not mirrored.

Role tokens only. Current: primary-colored indicator border and number, semibold label. Complete: filled primary indicator with a check, primary connector to the next step. Error: error-foreground indicator with an alert icon. Disabled: disabled text color. Forced-colors mode: CanvasText indicators and connectors, Highlight complete indicator, GrayText disabled steps.

configureAbsoluteUI({ components: { stepper: { orientation: 'vertical', linear: false } } });

Changes apply to existing instances immediately (the list is re-rendered).

Instance options override global and theme defaults. All options are dynamic through update().

Slot Default Used for
icons.complete check Complete step indicator.
icons.error alert-circle Error step indicator.

Upcoming and current steps show their 1-based number. Icons resolve through the global icon provider/registry.

Labels and descriptions use textContent. Each href is parsed with the URL standard against document.baseURI; only http:/https: and scheme-less URLs (relative, path-absolute, query, fragment) are accepted. javascript:, data:, and other schemes, including obfuscations such as java\tscript:, throw STEPPER_OPTIONS; the error names the property but never echoes the value. CSP-safe.

No persistence, network access, or telemetry. status is consumer data; the stepper reads it and never changes it.

One click listener on the list and one configuration subscription per instance. Every change rebuilds the step list (O(steps)); focus is restored to the same step. Budget: Stepper.create with 10 steps under 5 ms in headless Chromium.

Designed for 2-7 steps; any count renders, but horizontal steps share the width, so long flows switch to the vertical layout early. Tested with 4-step flows.

  • Linear rule: a step after the current one is reachable only when every step before it, including the current step, has status: 'complete'. An 'error' step blocks forward reach past it.
  • Earlier steps are always reachable unless disabled, in both modes.
  • The current step is shown as current even when its status is set; its status text is not rendered.
  • A disabled step can still be current (through value, the default first step, or select()).
  • select() and update({ value }) bypass reachability.
  • A single-step stepper has no reachable steps.
  • A current or unreachable step with an href renders as text, not as a link.
  • optional shows a hint only; it does not change reachability.
  • Re-renders (update(), configuration changes) rebuild the list but keep focus on the same step; a step that became current or unreachable is focused as text (tabindex="-1").
Code Category Cause
STEPPER_OPTIONS configuration Empty label; empty steps; an empty or duplicate step id; an empty step label; status other than 'complete'/'error'; an unsafe or unparsable href; value (or select(id)) not matching a step. property is stepper.<name>, for example stepper.steps.href.
CFG_INVALID_VALUE configuration Invalid orientation, linear, or direction (instance or global configuration).

A failed construction leaves the host untouched: no children, classes, listeners, or subscriptions. A failed update() keeps the previous options and rendering.

  • The list is rebuilt on every change; focus inside the stepper is not restored after update() or a configuration change.
  • After a link step is activated without preventDefault(), focus is not moved (the page is expected to navigate).
  • The horizontal-to-vertical switch is fixed at a 30rem container width.
  • No arrow-key navigation.
  • Screen-reader behavior has not yet been manually verified with NVDA, JAWS, and VoiceOver.
  • HTML or custom nodes in labels and descriptions.
  • Hrefs with schemes other than http and https.
  • Branching or nested steps.
  • Automatic completion tracking.