Open demo

CSS code

HTML
<div class="w">
  <div class="steps" role="radiogroup" aria-label="Setup step">
    <input type="radio" name="w" id="w1"><label class="done" for="w1"><span class="dot" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg></span><b>Workspace</b><small>Created</small></label>
    <input type="radio" name="w" id="w2"><label class="done" for="w2"><span class="dot" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg></span><b>Invite team</b><small>4 sent</small></label>
    <input type="radio" name="w" id="w3" checked><label for="w3"><span class="dot" aria-hidden="true">3</span><b>Connect a repo</b><small>In progress</small></label>
    <input type="radio" name="w" id="w4" disabled><label class="locked" for="w4"><span class="dot" aria-hidden="true">4</span><b>First board</b><small>Locked</small></label>
  </div>
  <div class="body">
    <div class="p p1"><h3>Workspace created</h3><p>Named, and the region set to eu-west. This can be changed later from settings.</p></div>
    <div class="p p2"><h3>Team invited</h3><p>Four invitations sent. People can join before the remaining steps are finished.</p></div>
    <div class="p p3"><h3>Connect a repository</h3><p>Pick the repo this board should track. Completed steps stay clickable so you can go back; the step ahead is disabled at the input, not just styled to look it.</p></div>
    <div class="p p4"><h3>First board</h3><p class="lock"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="10" width="16" height="10" rx="2"/><path d="M8 10V7a4 4 0 018 0v3"/></svg> Finish connecting a repository first.</p></div>
  </div>
</div>
CSS
/* Wizard tabs, CSS only: the steps are a radio group. Completed steps stay clickable, the current one is checked,
   and the step ahead is a disabled radio, so it cannot be reached. :has() shows the panel for the checked step. */
.w{width:100%;max-width:600px;padding:24px 26px 26px;background:#fff;border:1px solid #e3e8f2;border-radius:16px;
  box-shadow:0 16px 38px -24px rgba(16,24,48,.24)}
.steps{display:flex;margin-bottom:22px;position:relative}
.steps input{position:absolute;opacity:0;width:1px;height:1px;margin:0;pointer-events:none}
.steps label{flex:1;position:relative;padding-top:32px;text-align:center;cursor:pointer;border-radius:8px}
.steps label::before{content:"";position:absolute;top:11px;left:0;right:0;height:3px;background:#e3e8f2}
.steps label:first-of-type::before{left:50%}
.steps label:last-of-type::before{right:50%}
.dot{position:absolute;top:0;left:50%;transform:translateX(-50%);width:25px;height:25px;
  border-radius:50%;display:grid;place-items:center;font-size:.7rem;font-weight:900;
  background:#fff;border:3px solid #e3e8f2;color:#64708a;z-index:1}
.dot svg{width:11px;height:11px;stroke:currentColor;fill:none;stroke-width:3.4}
.steps b{display:block;font-size:.83rem;color:#5d6a86}
.steps small{font-size:.72rem;color:#64708a}
.steps .done::before{background:#4f46e5}
.steps .done .dot{background:#4f46e5;border-color:#4f46e5;color:#fff}
.steps .done b{color:#4a5670}
.locked{cursor:not-allowed}
.locked b,.locked small,.locked .dot{color:#c3cad8}
.steps input:checked+label .dot{border-color:#4f46e5;color:#4f46e5;box-shadow:0 0 0 4px #eef2ff}
.steps input:checked+.done .dot{color:#fff}
.steps input:checked+label b{color:#16203a}
.steps input:focus-visible+label{outline:2px solid #4f46e5;outline-offset:2px}
.body{padding-top:20px;border-top:1px solid #eef1f7}
.body .p{display:none;font-size:.9rem;line-height:1.7;color:#4a5670}
.body h3{font-size:.95rem;color:#16203a;margin:0 0 6px}
.body p{margin:0}
.body .lock{display:flex;align-items:center;gap:9px;font-size:.85rem;color:#5d6a86}
.body .lock svg{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:2}
.w:has(#w1:checked) .p1,.w:has(#w2:checked) .p2,.w:has(#w3:checked) .p3,.w:has(#w4:checked) .p4{display:block}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<div class="w">
  <div class="nav steps" role="tablist" aria-label="Setup step">
    <button class="step done" id="step-workspace-tab" data-bs-toggle="tab" data-bs-target="#step-workspace" type="button" role="tab" aria-controls="step-workspace" aria-selected="false" tabindex="-1"><span class="dot" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg></span><b>Workspace</b><small>Created</small></button>
    <button class="step done" id="step-team-tab" data-bs-toggle="tab" data-bs-target="#step-team" type="button" role="tab" aria-controls="step-team" aria-selected="false" tabindex="-1"><span class="dot" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg></span><b>Invite team</b><small>4 sent</small></button>
    <button class="step active" id="step-repo-tab" data-bs-toggle="tab" data-bs-target="#step-repo" type="button" role="tab" aria-controls="step-repo" aria-selected="true"><span class="dot" aria-hidden="true">3</span><b>Connect a repo</b><small>In progress</small></button>
    <button class="step" id="step-board-tab" data-bs-toggle="tab" data-bs-target="#step-board" type="button" role="tab" aria-controls="step-board" aria-selected="false" tabindex="-1" disabled><span class="dot" aria-hidden="true">4</span><b>First board</b><small>Locked</small></button>
  </div>
  <div class="tab-content body">
    <div class="tab-pane" id="step-workspace" role="tabpanel" aria-labelledby="step-workspace-tab" tabindex="0"><h3>Workspace created</h3><p>Named, and the region set to eu-west. This can be changed later from settings.</p></div>
    <div class="tab-pane" id="step-team" role="tabpanel" aria-labelledby="step-team-tab" tabindex="0"><h3>Team invited</h3><p>Four invitations sent. People can join before the remaining steps are finished.</p></div>
    <div class="tab-pane active" id="step-repo" role="tabpanel" aria-labelledby="step-repo-tab" tabindex="0"><h3>Connect a repository</h3><p>Pick the repo this board should track. Completed steps stay clickable so you can go back; the step ahead is disabled at the button, not just styled to look it.</p></div>
    <div class="tab-pane" id="step-board" role="tabpanel" aria-labelledby="step-board-tab" tabindex="0"><h3>First board</h3><p class="lock"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="10" width="16" height="10" rx="2"/><path d="M8 10V7a4 4 0 018 0v3"/></svg> Finish connecting a repository first.</p></div>
  </div>
</div>
CSS
/* Wizard tabs: a Bootstrap .nav of step buttons driven by the Tab plugin. Completed steps stay selectable,
   and the step ahead is a disabled button, which the Tab plugin skips for clicks and arrow keys. */
.w{width:100%;max-width:600px;padding:24px 26px 26px;background:#fff;border:1px solid #e3e8f2;border-radius:16px;
  box-shadow:0 16px 38px -24px rgba(16,24,48,.24)}
.steps{flex-wrap:nowrap;margin-bottom:22px}
.steps .step{flex:1;position:relative;padding:32px 0 0;text-align:center;background:none;border:0;border-radius:8px;line-height:normal}
.steps .step::before{content:"";position:absolute;top:11px;left:0;right:0;height:3px;background:#e3e8f2}
.steps .step:first-child::before{left:50%}
.steps .step:last-child::before{right:50%}
.steps .step:focus-visible{outline:2px solid #4f46e5;outline-offset:2px}
.dot{position:absolute;top:0;left:50%;transform:translateX(-50%);width:25px;height:25px;
  border-radius:50%;display:grid;place-items:center;font-size:.7rem;font-weight:900;
  background:#fff;border:3px solid #e3e8f2;color:#64708a;z-index:1}
.dot svg{width:11px;height:11px;stroke:currentColor;fill:none;stroke-width:3.4}
.steps b{display:block;font-size:.83rem;color:#5d6a86}
.steps small{font-size:.72rem;color:#64708a}
.steps .done::before{background:#4f46e5}
.steps .done .dot{background:#4f46e5;border-color:#4f46e5;color:#fff}
.steps .done b{color:#4a5670}
.steps .step:disabled{cursor:not-allowed}
.steps .step:disabled b,.steps .step:disabled small,.steps .step:disabled .dot{color:#c3cad8}
.steps .step.active .dot{border-color:#4f46e5;color:#4f46e5;box-shadow:0 0 0 4px #eef2ff}
.steps .step.done.active .dot{color:#fff}
.steps .step.active b{color:#16203a}
.body{padding-top:20px;border-top:1px solid #eef1f7}
.body .tab-pane{font-size:.9rem;line-height:1.7;color:#4a5670}
.body .tab-pane:focus-visible{outline:2px solid #4f46e5;outline-offset:4px;border-radius:4px}
.body h3{font-size:.95rem;font-weight:700;line-height:inherit;color:#16203a;margin:0 0 6px}
.body p{margin:0}
.body .lock{display:flex;align-items:center;gap:9px;font-size:.85rem;color:#5d6a86}
.body .lock svg{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:2}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="w-full max-w-[600px] rounded-2xl border border-tab-line bg-white px-[26px] pt-6 pb-[26px] shadow-[0_16px_38px_-24px_rgba(16,24,48,.24)]">
  <div class="mb-[22px] flex" role="tablist" aria-label="Setup step">
    <button type="button" role="tab" id="step-workspace-tab" aria-controls="step-workspace" aria-selected="false" tabindex="-1" class="group relative flex-1 cursor-pointer rounded-lg pt-8 text-center before:absolute before:top-[11px] before:right-0 before:left-0 before:h-[3px] before:bg-tab-accent first:before:left-1/2 last:before:right-1/2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab-accent"><span class="absolute top-0 left-1/2 z-[1] grid size-[25px] -translate-x-1/2 place-items-center rounded-full border-3 border-tab-accent bg-tab-accent text-white group-aria-selected:shadow-[0_0_0_4px_var(--color-tab-tint)]" aria-hidden="true"><svg class="size-[11px] fill-none stroke-current stroke-[3.4]" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg></span><b class="block text-[.83rem] text-tab-body group-aria-selected:text-tab-ink">Workspace</b><small class="text-[.72rem] text-tab-hint">Created</small></button>
    <button type="button" role="tab" id="step-team-tab" aria-controls="step-team" aria-selected="false" tabindex="-1" class="group relative flex-1 cursor-pointer rounded-lg pt-8 text-center before:absolute before:top-[11px] before:right-0 before:left-0 before:h-[3px] before:bg-tab-accent first:before:left-1/2 last:before:right-1/2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab-accent"><span class="absolute top-0 left-1/2 z-[1] grid size-[25px] -translate-x-1/2 place-items-center rounded-full border-3 border-tab-accent bg-tab-accent text-white group-aria-selected:shadow-[0_0_0_4px_var(--color-tab-tint)]" aria-hidden="true"><svg class="size-[11px] fill-none stroke-current stroke-[3.4]" viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg></span><b class="block text-[.83rem] text-tab-body group-aria-selected:text-tab-ink">Invite team</b><small class="text-[.72rem] text-tab-hint">4 sent</small></button>
    <button type="button" role="tab" id="step-repo-tab" aria-controls="step-repo" aria-selected="true" class="group relative flex-1 cursor-pointer rounded-lg pt-8 text-center before:absolute before:top-[11px] before:right-0 before:left-0 before:h-[3px] before:bg-tab-line first:before:left-1/2 last:before:right-1/2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab-accent"><span class="absolute top-0 left-1/2 z-[1] grid size-[25px] -translate-x-1/2 place-items-center rounded-full border-3 border-tab-line bg-white text-[.7rem] font-black text-tab-hint group-aria-selected:border-tab-accent group-aria-selected:text-tab-accent group-aria-selected:shadow-[0_0_0_4px_var(--color-tab-tint)]" aria-hidden="true">3</span><b class="block text-[.83rem] text-tab-muted group-aria-selected:text-tab-ink">Connect a repo</b><small class="text-[.72rem] text-tab-hint">In progress</small></button>
    <button type="button" role="tab" id="step-board-tab" aria-controls="step-board" aria-selected="false" tabindex="-1" disabled class="group relative flex-1 cursor-not-allowed rounded-lg pt-8 text-center before:absolute before:top-[11px] before:right-0 before:left-0 before:h-[3px] before:bg-tab-line first:before:left-1/2 last:before:right-1/2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab-accent"><span class="absolute top-0 left-1/2 z-[1] grid size-[25px] -translate-x-1/2 place-items-center rounded-full border-3 border-tab-line bg-white text-[.7rem] font-black text-tab-locked" aria-hidden="true">4</span><b class="block text-[.83rem] text-tab-locked">First board</b><small class="text-[.72rem] text-tab-locked">Locked</small></button>
  </div>
  <div class="border-t border-[#eef1f7] pt-5 text-[.9rem] leading-[1.7] text-tab-body">
    <div id="step-workspace" role="tabpanel" aria-labelledby="step-workspace-tab" tabindex="0" hidden class="rounded focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab-accent"><h3 class="mb-1.5 text-[.95rem] font-bold text-tab-ink">Workspace created</h3><p>Named, and the region set to eu-west. This can be changed later from settings.</p></div>
    <div id="step-team" role="tabpanel" aria-labelledby="step-team-tab" tabindex="0" hidden class="rounded focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab-accent"><h3 class="mb-1.5 text-[.95rem] font-bold text-tab-ink">Team invited</h3><p>Four invitations sent. People can join before the remaining steps are finished.</p></div>
    <div id="step-repo" role="tabpanel" aria-labelledby="step-repo-tab" tabindex="0" class="rounded focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab-accent"><h3 class="mb-1.5 text-[.95rem] font-bold text-tab-ink">Connect a repository</h3><p>Pick the repo this board should track. Completed steps stay clickable so you can go back; the step ahead is disabled at the button, not just styled to look it.</p></div>
    <div id="step-board" role="tabpanel" aria-labelledby="step-board-tab" tabindex="0" hidden class="rounded focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab-accent"><h3 class="mb-1.5 text-[.95rem] font-bold text-tab-ink">First board</h3><p class="flex items-center gap-[9px] text-[.85rem] text-tab-muted"><svg class="size-[15px] fill-none stroke-current stroke-2" viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="10" width="16" height="10" rx="2"/><path d="M8 10V7a4 4 0 018 0v3"/></svg> Finish connecting a repository first.</p></div>
  </div>
</div>
CSS
@theme {
  --color-tab-ink: #16203a;
  --color-tab-body: #4a5670;
  --color-tab-muted: #5d6a86;
  --color-tab-hint: #64708a;
  --color-tab-locked: #c3cad8;
  --color-tab-line: #e3e8f2;
  --color-tab-tint: #eef2ff;
  --color-tab-accent: #4f46e5;
}
JavaScript
// WAI-ARIA tabs: click or arrow keys select a tab, Home/End jump to the ends, only the selected tab is in the Tab order.
for (const list of document.querySelectorAll('[role=tablist]')) {
  const tabs = [...list.querySelectorAll('[role=tab]')];
  const select = tab => tabs.forEach(t => {
    const on = t === tab;
    t.setAttribute('aria-selected', on);
    t.tabIndex = on ? 0 : -1;
    document.getElementById(t.getAttribute('aria-controls')).hidden = !on;
  });
  list.addEventListener('click', e => { const t = e.target.closest('[role=tab]'); if (t && !t.disabled) select(t); });
  list.addEventListener('keydown', e => {
    const live = tabs.filter(t => !t.disabled), i = live.indexOf(e.target);
    const n = { ArrowLeft: i - 1, ArrowUp: i - 1, ArrowRight: i + 1, ArrowDown: i + 1, Home: 0, End: live.length - 1 }[e.key];
    if (n === undefined || i < 0) return;
    e.preventDefault();
    const t = live[(n + live.length) % live.length];
    t.focus();
    select(t);
  });
}

About this set

Tabs drawn as a four-step setup wizard: Workspace and Invite team are complete and show a tick, Connect a repo is the current step, and First board is locked. A connecting line runs through the step dots and turns indigo up to the last completed step. Use it for onboarding, checkout or setup flows where people should be able to go back to finished steps but not skip ahead. The locked step is disabled in the markup rather than only styled grey: in the plain CSS version it is a disabled radio, in the Bootstrap and Tailwind versions a disabled tab button, so it cannot be clicked, focused or reached with the arrow keys. Each step’s small status line (Created, 4 sent, In progress, Locked) is part of its name, so assistive technology hears the state as well as the title. The plain CSS version picks the panel with :has(); the Bootstrap version is a .nav of step buttons driven by the Tab plugin; the Tailwind version uses group-aria-selected utilities and a small WAI-ARIA tabs script.

What’s included

  • Completed, current and locked steps with a progress line
  • The locked step is disabled, not just styled
  • Completed steps stay selectable so people can go back
  • Status text such as In progress is part of each step's name
  • Grey step text darkened to pass 4.5:1

Accessibility

In the plain CSS version the tabs are a radio group, not ARIA tabs: the radios sit inside an element with role="radiogroup" and an aria-label, each takes its name from its visible label, Tab enters the group and the arrow keys move the selection (radios have no Home or End). In the Bootstrap and Tailwind versions they follow the WAI-ARIA tabs pattern: role tablist, tab and tabpanel, aria-selected, aria-controls, a roving tabindex, arrow keys plus Home and End. The disabled step is skipped by keyboard and pointer in every version. The step dots are aria-hidden and focus shows a 2px indigo outline around the whole step.

Customise it

Change #4f46e5 for the progress line, dots and ring in the .steps rules (plain CSS and Bootstrap) or the --color-tab-accent and --color-tab-tint theme colours (Tailwind). Add a step by adding a radio and label, or a tab button and panel, and moving the done class along.