Stepper
stimeo--stepper
Wizard step navigation with derived state, aria-current, and a linear guard.
The stimeo--stepper controller manages a multi-step flow. There is no dedicated APG widget, so the current step is expressed with aria-current="step" on its operable button, and each step li gets a data-state (complete / current / upcoming) derived from the current index. next/prev move one step (ignored past either end) and goto jumps to a step via its index param; with linear=true, goto cannot skip more than one step ahead (going back is always allowed). Each move dispatches stimeo--stepper:change. Step content, validation, and the look of circles/lines/numbers are yours.
Keyboard
| Key | Action |
|---|---|
| Enter / Space | Activate a step button (goto) — native button behavior. |
| Tab | Each step button is in the natural tab order (no roving/trap). |
<%# Markup for the stepper (steps / wizard) demo.
The steps (ol + button) and the prev/next buttons are grouped under one controller
element. The library manages the current step number, sets data-state (complete /
current / upcoming) on each li, sets aria-current="step" on the current step's button,
enforces out-of-range / linear constraints, and fires the change event. The circle /
line / number look is in demo.css. %>
<div
class="stepper"
data-controller="stimeo--stepper"
data-stimeo--stepper-index-value="0">
<ol class="stepper__list">
<% %w[account profile review].each_with_index do |step, index| %>
<li class="stepper__step" data-stimeo--stepper-target="step">
<button
type="button"
class="stepper__button"
<%= "aria-current=\"step\"".html_safe if index.zero? %>
data-stimeo--stepper-index-param="<%= index %>"
data-action="click->stimeo--stepper#goto">
<span class="stepper__marker"><%= index + 1 %></span>
<span class="stepper__label"><%= t("components.stepper.demo.steps.#{step}") %></span>
</button>
</li>
<% end %>
</ol>
<div class="stepper__controls">
<button type="button" class="demo-trigger" data-action="click->stimeo--stepper#prev">
<%= t("components.stepper.demo.back") %>
</button>
<button type="button" class="demo-trigger" data-action="click->stimeo--stepper#next">
<%= t("components.stepper.demo.continue") %>
</button>
</div>
</div>
/*
* Presentation-only styles for the stepper demo.
* Complete / current / upcoming are built by reacting to each li's data-state
* (complete / current / upcoming) and the current button's aria-current="step". The
* state transitions are the library's.
*/
.stepper__list {
display: flex;
gap: 0;
margin: 0;
padding: 0;
list-style: none;
}
.stepper__step {
flex: 1;
position: relative;
text-align: center;
}
.stepper__step:not(:last-child)::after {
content: "";
position: absolute;
top: 1rem;
left: 50%;
width: 100%;
height: 2px;
background: var(--border-strong);
}
.stepper__step[data-state="complete"]::after {
background: var(--accent, var(--color-primary));
}
.stepper__button {
position: relative;
z-index: 1;
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
padding: 0;
border: 0;
background: none;
color: var(--color-text-muted);
font: inherit;
cursor: pointer;
}
.stepper__marker {
display: grid;
place-items: center;
width: 2rem;
height: 2rem;
border-radius: 50%;
border: 2px solid var(--border-strong);
background: var(--surface, var(--surface-card));
font-weight: 600;
}
.stepper__step[data-state="complete"] .stepper__marker {
border-color: var(--accent, var(--color-primary));
background: var(--accent, var(--color-primary));
color: var(--white);
}
.stepper__step[data-state="current"] .stepper__marker {
border-color: var(--accent, var(--color-primary));
color: var(--accent, var(--color-primary));
}
.stepper__button[aria-current="step"] {
color: var(--fg, var(--color-text));
font-weight: 600;
}
.stepper__controls {
display: flex;
gap: 0.5rem;
margin-top: 1.25rem;
}
This demo needs no consumer-side JS (the controller handles the behavior).
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--stepper"
Targets
| Name | Description | Attribute |
|---|---|---|
step
required
|
A step item whose data-state (complete/current/upcoming) is derived from the current index. |
data-stimeo--stepper-target="step" |
Values
| Name | Description | Attribute |
|---|---|---|
index
|
The current step index, clamped into range on connect (default 0). | data-stimeo--stepper-index-value |
linear
|
When true, goto may not skip more than one step ahead; backward moves stay allowed (default false). | data-stimeo--stepper-linear-value |
Actions
| Name | Description | Action |
|---|---|---|
goto
|
Jumps to the step carried in the action's index param (subject to the linear rule). | stimeo--stepper#goto |
next
|
Advances one step, ignored at the last step. | stimeo--stepper#next |
prev
|
Goes back one step, ignored at the first step. | stimeo--stepper#prev |
Events
| Name | Description | Event |
|---|---|---|
change
|
Fires after a step move; detail carries index, previous, and the step element. | stimeo--stepper:change |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
data-state |
Step (li) | "complete" / "current" / "upcoming", derived from the current index. |
aria-current |
Current step button | "step" on the current step's button only. |