Onboarding Stepper
A horizontal progress stepper for sign-up or checkout flows: done, current and upcoming states with a filling track.
Preview and code
index.html
<div class="st">
<ol class="st-list" style="--p:0">
<li>Account</li><li>Workspace</li><li>Invite team</li><li>Done</li>
</ol>
<p class="st-copy" aria-live="polite"></p>
<div class="st-nav"><button type="button" data-d="-1">Back</button><button type="button" data-d="1" class="primary">Next</button></div>
</div>
styles.css
.st{width:min(440px,100%);display:grid;gap:16px;font-size:13px}
.st-list{position:relative;display:grid;grid-template-columns:repeat(4,1fr);margin:0;padding:0;list-style:none;counter-reset:s}
.st-list::before,.st-list::after{content:"";position:absolute;top:15px;left:12.5%;height:2px;background:var(--line);width:75%}
.st-list::after{background:var(--accent);width:calc(75% * var(--p));transition:width .45s cubic-bezier(.2,.8,.2,1)}
.st-list li{position:relative;z-index:1;display:grid;justify-items:center;gap:6px;color:var(--muted);counter-increment:s;text-align:center}
.st-list li::before{content:counter(s);width:32px;height:32px;border-radius:50%;display:grid;place-items:center;font-weight:700;background:var(--stage);border:2px solid var(--line);transition:all .3s}
.st-list li.done::before{content:"✓";background:var(--accent);border-color:var(--accent);color:#fff}
.st-list li[aria-current=step]{color:var(--ink);font-weight:600}
.st-list li[aria-current=step]::before{border-color:var(--accent);color:var(--accent);box-shadow:0 0 0 4px color-mix(in srgb,var(--accent) 18%,transparent)}
.st-copy{margin:0;text-align:center;color:var(--muted);min-height:1.5em}
.st-nav{display:flex;justify-content:center;gap:8px}
.st-nav button{border:1px solid var(--line);background:var(--stage);color:var(--ink);border-radius:999px;padding:9px 18px;font:600 13px/1 system-ui,sans-serif;cursor:pointer}
.st-nav button.primary{background:var(--ink);color:var(--stage);border-color:var(--ink)}
.st-nav button:disabled{opacity:.4;cursor:default}
.st-nav button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
@media (prefers-reduced-motion:reduce){.st-list::after,.st-list li::before{transition:none}}
script.js
const list=document.querySelector('.st-list'),steps=[...list.children],copy=document.querySelector('.st-copy');
const text=['Create your account to get started.','Name your workspace.','Invite people you work with (optional).','All set. Welcome aboard!'];
const [back,next]=document.querySelectorAll('.st-nav button');let cur=0;
function show(){steps.forEach((s,i)=>{s.classList.toggle('done',i<cur||cur===steps.length-1&&i===cur);i===cur?s.setAttribute('aria-current','step'):s.removeAttribute('aria-current')});
list.style.setProperty('--p',cur/(steps.length-1));copy.textContent='Step '+(cur+1)+' of '+steps.length+': '+text[cur];back.disabled=cur===0;next.disabled=cur===steps.length-1}
document.querySelectorAll('.st-nav button').forEach(b=>b.onclick=()=>{cur=Math.max(0,Math.min(steps.length-1,cur+ +b.dataset.d));show()});show();
onboarding-stepper.tsx
"use client";
const css = `
.st-list{position:relative;display:grid;margin:0;padding:0;list-style:none;font:13px/1.4 system-ui,sans-serif}
.st-list::before,.st-list::after{content:"";position:absolute;top:15px;height:2px;background:#d8deeb}
.st-list::after{background:#1659f5;transition:width .45s cubic-bezier(.2,.8,.2,1)}
.st-list li{position:relative;z-index:1;display:grid;justify-items:center;gap:6px;color:#5c627c;text-align:center}
.st-dot{width:32px;height:32px;border-radius:50%;display:grid;place-items:center;font-weight:700;background:#fff;border:2px solid #d8deeb;transition:all .3s}
.st-list li.done .st-dot{background:#1659f5;border-color:#1659f5;color:#fff}
.st-list li[aria-current=step]{color:#0b1020;font-weight:600}
.st-list li[aria-current=step] .st-dot{border-color:#1659f5;color:#1659f5;box-shadow:0 0 0 4px rgba(22,89,245,.18)}
@media (prefers-reduced-motion:reduce){.st-list::after,.st-dot{transition:none}}`;
export function OnboardingStepper({ steps, current }: { steps: string[]; current: number }) {
const n = steps.length;
const edge = 50 / n;
const track = 100 - edge * 2;
const progress = n > 1 ? current / (n - 1) : 1;
const dynamic = `.st-list::before,.st-list::after{left:${edge}%}.st-list::before{width:${track}%}.st-list::after{width:${track * progress}%}`;
return (
<>
<style>{css + dynamic}</style>
<ol className="st-list" style={{ gridTemplateColumns: `repeat(${n}, 1fr)` }}>
{steps.map((label, i) => {
const done = i < current || (current === n - 1 && i === current);
return (
<li key={label} className={done ? "done" : undefined} aria-current={i === current ? "step" : undefined}>
<span className="st-dot">{done ? "✓" : i + 1}</span>
{label}
</li>
);
})}
</ol>
</>
);
}
Install
npx shadcn@latest add https://ownlyplans.com/r/bhaskar/onboarding-stepper.json
Adds the React files to components/ownlyplans/ in a project set up with shadcn and Tailwind CSS. Or copy them from the Code tab.
About
Shows people where they are in a multi-step flow and how much is left, which is one of the simplest ways to cut drop-off. Completed steps get a tick, the current step is marked with aria-current, and the track fills as you go.
Use Back and Next in the preview to try it. In the React version control it with the current prop.