Stepper
stimeo--stepper
Moves through a multi-step process, optionally stopping people skipping ahead.
Moves forward and back through a process split into steps. Which step you are on is announced, and whether a step is done, current or still to come is readable from CSS, so the look is yours. It steps one at a time and can also jump to a step by number. When the order matters you can stop anyone jumping ahead of the next step, while going back is always allowed. The content of each step, its validation, and the circles and lines 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);
}
/* The theme-aware accent *text* token: the raw brand accent measures ~2.9:1 on the
light canvas, under the 4.5:1 floor. It resolves back to the brand value in dark. */
.stepper__step[data-state="current"] .stepper__marker {
border-color: var(--accent, var(--color-primary));
color: var(--accent-700);
}
.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. |