Live: try it with the mouse and the keyboard (Tab, arrows, Escape).

Stepper UI component for websites and web apps, shown in Mintwell, a fictional finance product. Shows a five-step progress stepper with completed, current and upcoming steps, Back and Next buttons, and every step state (default, hover, focus-visible, current, disabled). Brutalist style: high contrast, Space Grotesk, uppercase headings, thick black borders and hard offset shadows. Comes with copyable HTML, CSS and a few lines of plain JavaScript (keyboard support and ARIA per the WAI-ARIA Authoring Practices), a live demo, and a DESIGN.md with light and dark colour tokens.
#fcfcfa · surface #ffffff · text #1c1a12 · muted #666358 · primary #7b6600 · accent #4e69c5#161510 · surface #292720 · text #eeedec · muted #b4b1a8 · primary #c5a932 · accent #b2c6ffPaste the HTML where the component goes, the CSS into your stylesheet and the script before </body>. The tokens block holds the light and dark colours; swap in your own.
<div class="mgd-stepper" data-step="3">
<ol class="mgd-steps" aria-label="Open a business account">
<li class="mgd-steps__item is-done"><span class="mgd-steps__dot" aria-hidden="true"><svg class="i" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span><span><span class="mgd-sr">Completed: </span>Business details</span></li>
<li class="mgd-steps__item is-done"><span class="mgd-steps__dot" aria-hidden="true"><svg class="i" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span><span><span class="mgd-sr">Completed: </span>Owners</span></li>
<li class="mgd-steps__item" aria-current="step"><span class="mgd-steps__dot" aria-hidden="true">3</span><span><span class="mgd-sr">Current: </span>Verify identity</span></li>
<li class="mgd-steps__item"><span class="mgd-steps__dot" aria-hidden="true">4</span><span><span class="mgd-sr">Not started: </span>Fund account</span></li>
<li class="mgd-steps__item"><span class="mgd-steps__dot" aria-hidden="true">5</span><span><span class="mgd-sr">Not started: </span>Review</span></li>
</ol>
<div class="mgd-steps__panel">
<h3 class="mgd-steps__title">Step 3 of 5: Verify identity</h3>
<p class="mgd-steps__text">A photo of a passport or driving licence and a quick selfie.</p>
<div class="mgd-steps__nav"><button class="mgd-btn" type="button" data-step-go="-1">Back</button><button class="mgd-btn mgd-btn--primary" type="button" data-step-go="1">Next</button></div>
</div>
<p class="mgd-sr" aria-live="polite" data-step-live></p>
</div>/* Tokens (light + dark) */
:root{--bg:#fcfcfa;--surface:#ffffff;--border:#dddbd1;--text:#1c1a12;--muted:#666358;--primary:#7b6600;--on-primary:#ffffff;--accent:#4e69c5;--font-h:"Space Grotesk",Georgia,serif;--font-b:"Space Grotesk",system-ui,sans-serif;--hw:700;
--c-bw:2px;--c-line:var(--text);--c-r:0;--c-pill:0;--c-sh:3px 3px 0 var(--text);--c-pop:6px 6px 0 var(--text);--c-tt:uppercase;--c-ls:.02em;--c-fw:700;--c-fs:13px;--c-ring:3px solid var(--text);--c-ind:4px;color-scheme:light}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--bg:#161510;--surface:#292720;--border:#403e36;--text:#eeedec;--muted:#b4b1a8;--primary:#c5a932;--on-primary:#111111;--accent:#b2c6ff;color-scheme:dark}
}
:root[data-theme="dark"]{--bg:#161510;--surface:#292720;--border:#403e36;--text:#eeedec;--muted:#b4b1a8;--primary:#c5a932;--on-primary:#111111;--accent:#b2c6ff;color-scheme:dark}
/* Component */
.mgd-sr{position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
.mgd-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:40px;padding:0 16px;border:var(--c-bw) solid var(--c-line);border-radius:var(--c-pill);background:var(--surface);color:var(--text);font:var(--c-fw) var(--c-fs)/1 var(--font-b);text-transform:var(--c-tt);letter-spacing:var(--c-ls);box-shadow:var(--c-sh);cursor:pointer;white-space:nowrap;transition:background-color .12s,border-color .12s}
.mgd-btn:hover,.mgd-btn.is-hover{background:color-mix(in oklab,var(--primary) 9%,var(--surface))}
.mgd-btn:focus-visible,.mgd-btn.is-focus{outline:var(--c-ring);outline-offset:2px}
.mgd-btn[aria-expanded="true"],.mgd-btn.is-open{background:color-mix(in oklab,var(--primary) 15%,var(--surface))}
.mgd-btn--primary{background:var(--primary);border-color:var(--primary);color:var(--on-primary)}
.mgd-btn--primary:hover,.mgd-btn--primary.is-hover{background:color-mix(in oklab,var(--primary) 84%,var(--text))}
.mgd-btn:disabled,.mgd-btn[aria-disabled="true"],.mgd-btn.is-disabled{opacity:.5;cursor:not-allowed;box-shadow:none}
.mgd-btn svg{flex:none}
.mgd-steps{list-style:none;margin:0 0 22px;padding:0;display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px;counter-reset:step}
.mgd-steps__item{position:relative;display:grid;justify-items:center;gap:8px;text-align:center;font:500 13px/1.3 var(--font-b);color:var(--muted);text-transform:var(--c-tt);letter-spacing:var(--c-ls)}
.mgd-steps>.mgd-steps__item:not(:first-child)::before{content:"";position:absolute;top:17px;right:calc(50% + 24px);width:calc(100% - 40px);height:3px;background:var(--border)}
.mgd-steps>.mgd-steps__item.is-done:not(:first-child)::before,.mgd-steps>.mgd-steps__item[aria-current="step"]:not(:first-child)::before{background:var(--primary)}
.mgd-steps__dot{width:36px;height:36px;display:grid;place-items:center;border-radius:0;border:2px solid var(--text);background:var(--bg);color:var(--text);font:600 14px/1 var(--font-b);font-variant-numeric:tabular-nums;transition:background-color .14s,border-color .14s}
.mgd-steps__item.is-done .mgd-steps__dot{background:var(--primary);border-color:var(--primary);color:var(--on-primary)}
.mgd-steps__item[aria-current="step"],.mgd-steps__item.is-on{color:var(--text);font-weight:700}
.mgd-steps__item[aria-current="step"] .mgd-steps__dot,.mgd-steps__item.is-on .mgd-steps__dot{border-color:var(--primary);box-shadow:0 0 0 4px color-mix(in oklab,var(--primary) 22%,transparent)}
.mgd-steps__item.is-hover .mgd-steps__dot{background:color-mix(in oklab,var(--primary) 10%,var(--bg))}
.mgd-steps__item.is-focus .mgd-steps__dot{outline:var(--c-ring);outline-offset:2px}
.mgd-steps__item.is-off{opacity:.45}
.mgd-steps__panel{max-width:560px;padding:18px;background:var(--surface);border:var(--c-bw) solid var(--c-line);border-radius:var(--c-r);box-shadow:var(--c-sh)}
.mgd-steps__panel h3{margin:0 0 4px;font:var(--hw) 19px/1.25 var(--font-h)}
.mgd-steps__panel p{margin:0;color:var(--muted)}
.mgd-steps__nav{display:flex;gap:10px;margin-top:16px}
@media (max-width:640px){.mgd-steps{grid-template-columns:1fr;gap:0}
.mgd-steps__item{grid-template-columns:36px 1fr;justify-items:start;align-items:center;text-align:left;padding:0 0 18px}
.mgd-steps>.mgd-steps__item:not(:first-child)::before{top:auto;bottom:calc(100% - 0px);right:auto;left:17px;width:2px;height:18px}}
@media (prefers-reduced-motion:reduce){.mgd-steps,.mgd-steps *{transition:none!important;animation-duration:2.4s!important}}document.querySelectorAll(".mgd-stepper").forEach(function (box) {
var items = [].slice.call(box.querySelectorAll(".mgd-steps__item")), n = items.length, live = box.querySelector("[data-step-live]");
var texts = ["Legal name, address and what the business does.","Everyone who owns 25% or more.","A photo of a passport or driving licence and a quick selfie.","Move at least $1 in from another bank.","Check everything once, then open the account."];
var check = items[0].querySelector(".mgd-steps__dot").innerHTML;
function render(step, announce) {
box.dataset.step = step;
items.forEach(function (li, i) {
var done = i < step - 1, cur = i === step - 1;
li.classList.toggle("is-done", done);
if (cur) li.setAttribute("aria-current", "step"); else li.removeAttribute("aria-current");
li.querySelector(".mgd-steps__dot").innerHTML = done ? check : String(i + 1);
li.querySelector(".mgd-sr").textContent = done ? "Completed: " : cur ? "Current: " : "Not started: ";
});
var title = "Step " + step + " of " + n + ": " + items[step - 1].lastElementChild.lastChild.textContent;
box.querySelector(".mgd-steps__title").textContent = title;
box.querySelector(".mgd-steps__text").textContent = texts[step - 1];
var back = box.querySelector('[data-step-go="-1"]'), next = box.querySelector('[data-step-go="1"]');
back.setAttribute("aria-disabled", step === 1); next.textContent = step === n ? "Open account" : "Next";
if (announce) live.textContent = title;
}
box.addEventListener("click", function (e) {
var b = e.target.closest("[data-step-go]"); if (!b || b.getAttribute("aria-disabled") === "true") return;
var s = Math.min(n, Math.max(1, +box.dataset.step + +b.dataset.stepGo)); if (s !== +box.dataset.step) render(s, true);
});
render(+box.dataset.step, false);
});## Component: Stepper
Progress through a short multi-step task, with completed, current and upcoming steps and Back / Next buttons.
- **Pattern:** Semantic HTML (no ARIA widget role needed)
- **Roles and attributes:** An ordered list of steps; the current step has aria-current="step"; completed and upcoming states are spelled out in visually hidden text; a polite live region announces the step on Back / Next.
- **States drawn:** default, hover, focus-visible, selected, disabled
- **Focus:** a visible `:focus-visible` outline (3px solid text), never removed
- **Style knobs (brutalist):** border 2px, corners 0, shadow `3px 3px 0 var(--text)`, labels uppercase, tracked .02em
- **Motion:** transitions are short (≤160ms) and switched off under `prefers-reduced-motion: reduce`
- **Code:** HTML + CSS + a few lines of plain JavaScript (no libraries); colours come only from the tokens above, so light and dark both work
### Keyboard
- **Tab:** reach Back and Next
- **Enter / Space:** go back or forward one step
### Licence
Original Made Good Designs code: free to use and adapt in any project, including commercial ones, with no credit needed. Fonts are Google Fonts under their own licences.Add the Made Good Designs library to your assistant once. Then ask for UI references, brand palettes, font pairings and design tokens in any chat.
https://madegooddesigns.com/inspiration/mcp?ref=ui-stepper-uiSettings → Connectors → Add custom connector → paste the URL.
Settings → Apps & Connectors → Advanced → Developer mode → Create → paste the URL.
One click, then confirm in Cursor.
One click, then confirm in VS Code (Copilot agent mode).
claude mcp add --transport http made-good-designs https://madegooddesigns.com/inspiration/mcp?ref=ui-stepper-ui
gemini mcp add --transport http made-good-designs https://madegooddesigns.com/inspiration/mcp?ref=ui-stepper-ui
Add this to the app's MCP config (Windsurf: mcp_config.json).
Fonts: Space Grotesk (Google Fonts)
Prompt copied: paste it into the chat that just opened.
Writing about this component?
search_ui_screens (e.g. “billing settings, editorial style”), get_ui_flow for a whole journey, and get_ui_screen_code to pull the HTML and DESIGN.md straight into your project.