Fylgja Stepper
A stepper, also called a step pager, shows where the user is in a multi-step flow
such as a checkout or an onboarding wizard. It is a plain ordered list, so the step
numbers and the total count come from the list itself, and aria-current="step" marks
the current step for assistive tech.
Preview
- Cart (completed)
- Shipping (completed)
- Payment
- Review
<ol class="stepper flex gap-sm" role="list" aria-label="Checkout progress">
<li>Cart <span class="sr-only">(completed)</span></li>
<li>Shipping <span class="sr-only">(completed)</span></li>
<li aria-current="step">Payment</li>
<li>Review</li>
</ol>
<style>
.stepper {
--stepper-size: 2em;
--stepper-stroke: color-mix(in oklab, var(--root-bg) 75%, var(--root-fg));
counter-reset: step;
padding: 0;
li {
counter-increment: step;
position: relative;
display: flex;
flex: 1;
align-items: center;
gap: 0.5em;
}
li:last-child {
flex: none;
}
li::before {
content: counter(step);
display: grid;
place-content: center;
flex: none;
inline-size: var(--stepper-size);
aspect-ratio: 1;
border: 2px solid var(--stepper-stroke);
border-radius: 50%;
font-variant-numeric: tabular-nums;
}
li:not(:last-child)::after {
content: "";
flex: 1;
min-inline-size: 1em;
block-size: 2px;
background: var(--stepper-stroke);
}
li:has(~ [aria-current="step"])::before {
content: "\2713" / "";
border-color: var(--brand);
background: var(--brand);
color: var(--on-brand);
}
li:has(~ [aria-current="step"])::after {
background: var(--brand);
}
[aria-current="step"] {
font-weight: 600;
}
button {
--btn-stroke: transparent;
--btn-px: 0.5em;
}
[aria-current="step"]::before {
border-color: var(--brand);
}
&.--vertical li {
flex: none;
padding-block-end: 1.5em;
}
&.--vertical li:last-child {
padding-block-end: 0;
}
&.--vertical li:not(:last-child)::after {
position: absolute;
inset-block: var(--stepper-size) 0;
inset-inline-start: calc(var(--stepper-size) / 2 - 1px);
inline-size: 2px;
block-size: auto;
min-inline-size: 0;
}
}
</style>Usage
- List the steps in an
<ol class="stepper flex gap-sm">, one<li>per step, in order. - Mark the current step with
aria-current="step". Steps before it are styled as completed, with a brand checkmark, and steps after it as upcoming. - Name the list with
aria-label, and add a visually hidden “(completed)” label to every completed step. - The small
.stepperstyle numbers the steps with a CSS counter, draws the round markers, and connects them with a line that turns brand colored once a step is done. - Change the marker size with
--stepper-sizeand the line color with--stepper-stroke.
Anatomy
| Part | Responsibility |
|---|---|
<ol class="stepper"> |
The ordered list of steps. Provides the order and the total count. |
<li> |
One step. Its ::before is the marker and ::after the connector. |
aria-current="step" |
Marks the current step. Everything before it reads as completed. |
.sr-only text |
Tells screen reader users a step is completed. |
<button> |
Optional. Lets users return to a completed step. |
Accessibility
- The ordered list announces the number of steps and each step’s position.
aria-current="step"is announced as the current step, so the state does not rely on color or the marker alone.- The checkmark uses empty alternative text in
content, so it is not read out. The hidden “(completed)” text carries that meaning instead. - A stepper navigates within one flow rather than across the site, so it needs no
<nav>landmark. Move focus to the new step’s heading when the user advances, so they know the content changed.
Variants
Interactive
When users may go back to a completed step, wrap its label in a <button>. The
stepper CSS removes the button border, so it reads as a flat button that still shows
its hover and focus states. Upcoming steps stay plain text. It uses the same CSS as
the preview above.
- Plan
- Confirm
<ol class="stepper flex gap-sm" role="list" aria-label="Sign up progress">
<li><button type="button">Account <span class="sr-only">(completed)</span></button></li>
<li><button type="button">Profile <span class="sr-only">(completed)</span></button></li>
<li aria-current="step">Plan</li>
<li>Confirm</li>
</ol>Vertical
Add the --vertical modifier and swap flex gap-sm for flex-col to stack the steps,
with the connector running down between the markers. This suits a sidebar or a narrow
screen. It uses the same CSS as the preview above.
- Create account (completed)
- Verify email
- Choose a plan
- Invite your team
<ol class="stepper --vertical flex-col" role="list" aria-label="Account setup">
<li>Create account <span class="sr-only">(completed)</span></li>
<li aria-current="step">Verify email</li>
<li>Choose a plan</li>
<li>Invite your team</li>
</ol>Related: Pagination, Progress, Breadcrumbs.
FAQ
How do I mark which steps are done?
You only mark the current step, with aria-current="step". The CSS treats every step before it as completed and every step after it as upcoming, so moving to the next step is a single attribute change.
Why is there hidden "(completed)" text?
The checkmark is drawn in CSS and hidden from assistive tech, so it carries no meaning on its own. The visually hidden text tells screen reader users which steps are done, while aria-current already announces the current one.
Should the steps be links?
No. A stepper navigates within one flow, not between pages, so links do not fit. When users may go back to a completed step, make that step a button and handle it in your flow, for example as a submit button with a name and value in the wizard form. Upcoming steps stay plain text, since they cannot be skipped to.
What about narrow screens?
A horizontal stepper with more than three or four labeled steps gets cramped on a phone. Switch to the vertical layout there, or shorten the labels.
Why does the list have role="list"?
Safari drops the list semantics of a list without markers. The role keeps the "list, 4 items" announcement, which tells users how many steps there are.