Step Indicator
stimeo--step-indicator
A read-only progress indicator with derived state and a progress-ratio custom property.
The stimeo--step-indicator controller is a read-only progress display. There is no dedicated APG widget, so the current position is expressed with aria-current="step" on the current list item, and each step li gets a data-state (complete / current / upcoming) derived from the current index. It also exposes overall progress as the --stimeo-step-indicator-ratio custom property (0–1) for CSS. The steps are not operable and never take focus; the current step is updated from an external step:set event (here driven by the Back/Advance buttons), which dispatches stimeo--step-indicator:change. For an interactive wizard use the Stepper. Behavior only — the look is yours.
- Cart
- Shipping
- Payment
- Confirm
<%# Markup for the step-indicator (read-only progress) demo.
The library holds the current step number, sets data-state on each li, sets
aria-current="step" on the current li, and updates the progress CSS variable
(--stimeo-step-indicator-ratio). The steps themselves aren't interactive, so the
current step is updated via the external step:set event (fired by demo.js from the
prev/next buttons). %>
<div class="step-indicator-demo">
<ol
class="step-indicator"
data-controller="stimeo--step-indicator"
aria-label="<%= t("components.step_indicator.demo.label") %>"
data-stimeo--step-indicator-current-value="1"
data-action="step:set->stimeo--step-indicator#setCurrent">
<% %w[cart shipping payment confirm].each_with_index do |step, index| %>
<li
class="step-indicator__step"
data-stimeo--step-indicator-target="step"
<%= "data-state=\"complete\"".html_safe if index < 1 %>
<%= "data-state=\"current\" aria-current=\"step\"".html_safe if index == 1 %>
<%= "data-state=\"upcoming\"".html_safe if index > 1 %>>
<span class="step-indicator__dot"></span>
<span class="step-indicator__label"><%= t(
"components.step_indicator.demo.steps.#{step}"
) %></span>
</li>
<% end %>
</ol>
<div class="step-indicator__controls">
<button type="button" class="demo-trigger" data-step-indicator-prev>
<%= t("components.step_indicator.demo.back") %>
</button>
<button type="button" class="demo-trigger" data-step-indicator-next>
<%= t("components.step_indicator.demo.advance") %>
</button>
</div>
</div>
/*
* Presentation-only styles for the step-indicator demo.
* Complete / current / upcoming are built by reacting to each li's data-state. The
* progress bar uses --stimeo-step-indicator-ratio (0–1, updated by the library) as its width.
*/
.step-indicator {
display: flex;
gap: 0;
margin: 0;
padding: 0;
list-style: none;
position: relative;
}
.step-indicator::before,
.step-indicator::after {
content: "";
position: absolute;
top: 0.5rem;
left: 0.5rem;
right: 0.5rem;
height: 2px;
background: var(--surface-subtle);
}
.step-indicator::after {
right: auto;
width: calc((100% - 1rem) * var(--stimeo-step-indicator-ratio, 0));
background: var(--accent, var(--color-primary));
transition: width 0.2s ease;
}
.step-indicator__step {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
font-size: 0.8125rem;
color: var(--color-text-subtle);
}
.step-indicator__dot {
position: relative;
z-index: 1;
width: 1rem;
height: 1rem;
border-radius: 50%;
background: var(--border-strong);
}
.step-indicator__step[data-state="complete"] .step-indicator__dot,
.step-indicator__step[data-state="current"] .step-indicator__dot {
background: var(--accent, var(--color-primary));
}
.step-indicator__step[data-state="current"] {
color: var(--fg, var(--color-text));
font-weight: 600;
}
.step-indicator__controls {
display: flex;
gap: 0.5rem;
margin-top: 1.25rem;
}
// step-indicator external-update demo (consumer-side JS).
//
// The core controller (stimeo--step-indicator) is read-only: it only holds the current
// step and syncs data-state / aria-current / the progress CSS variable (no interaction).
// Changing the current step is done externally via the step:set event by contract, so
// here the prev/next buttons read the current value and fire step:set with a clamped new index.
document.querySelectorAll('[data-controller~="stimeo--step-indicator"]').forEach((list) => {
const root = list.closest('.step-indicator-demo') ?? list.parentElement;
if (!root) return;
const total = list.querySelectorAll('[data-stimeo--step-indicator-target="step"]').length;
const attr = 'data-stimeo--step-indicator-current-value';
const move = (delta) => {
const current = Number(list.getAttribute(attr) ?? 0);
const next = Math.min(total - 1, Math.max(0, current + delta));
list.dispatchEvent(new CustomEvent('step:set', { detail: { current: next } }));
};
root.querySelector('[data-step-indicator-prev]')?.addEventListener('click', () => move(-1));
root.querySelector('[data-step-indicator-next]')?.addEventListener('click', () => move(1));
});
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--step-indicator"
Targets
| Name | Description | Attribute |
|---|---|---|
step
required
|
Each step element, given a data-state (complete/current/upcoming) and aria-current. |
data-stimeo--step-indicator-target="step" |
Values
| Name | Description | Attribute |
|---|---|---|
current
|
The 0-based index of the current step, clamped to the step set (default 0). | data-stimeo--step-indicator-current-value |
Actions
| Name | Description | Action |
|---|---|---|
setCurrent
|
Updates the current step from detail.current (0-based, clamped) and dispatches change. | stimeo--step-indicator#setCurrent |
Events
| Name | Description | Event |
|---|---|---|
change
|
Fires when the current step changes, with detail.current and detail.total. | stimeo--step-indicator: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 (li) | "step" on the current step only. |
--stimeo-step-indicator-ratio |
Controller element | Overall progress as a 0–1 ratio for CSS. |