/* Onboarding wizards: a progress indicator on top, one <section data-step> per step and a Back / Continue bar.
The script hides every step but the current one, sets data-state and aria-current="step" on the <ol data-stepper>,
fills the segments or the <progress> bar and moves focus to the new step's heading. Without JavaScript every step shows. */
.ob{--ob-accent:#6d28d9;--ob-dark:#5b21b6;--ob-soft:#ede9fe;--ob-pink:#db2777;--ob-ink:#1e1b2e;--ob-muted:#5c5670;--ob-field:#8a82a3;--ob-track:#e4ddf8;--ob-tint:#faf8ff;
background:#fff;border-radius:20px;padding:28px 32px 30px;box-shadow:0 1px 2px rgba(30,27,46,.06),0 16px 40px -18px rgba(76,29,149,.3);color:var(--ob-ink)}
.ob [hidden]{display:none!important}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.ob svg{fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.ob-brand{display:flex;align-items:center;gap:10px}
.ob-logo{width:34px;height:34px;flex:none;border-radius:10px;display:grid;place-items:center;background:var(--ob-accent);color:#fff}
.ob-logo svg{width:20px;height:20px}
.ob-title{margin:0;font-size:1.05rem;font-weight:700;letter-spacing:-.01em;line-height:1.3}
/* A: "Step 2 of 4" + Skip, then one bar segment per step (the segments are the stepper <li>s, names visually hidden) */
.ob-top{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:24px;min-height:34px}
.ob-pos{font-size:.85rem;font-weight:700;color:var(--ob-dark)}
.ob-skip{font:inherit;font-size:.88rem;font-weight:600;color:var(--ob-dark);background:none;border:0;border-radius:8px;padding:6px 10px;margin-right:-10px;cursor:pointer;text-decoration:underline;text-underline-offset:3px}
.ob-skip:hover{background:var(--ob-soft)}
.ob-skip:focus-visible{outline:2px solid var(--ob-accent);outline-offset:1px}
.ob-segs{list-style:none;margin:8px 0 0;padding:0;display:grid;grid-template-columns:repeat(4,1fr);gap:6px}
.ob-segs li{position:relative;height:6px;border-radius:99px;background:var(--ob-track);overflow:hidden}
.ob-segs li::after{content:"";position:absolute;inset:0;border-radius:inherit;background:var(--ob-accent);transform:scaleX(0);transform-origin:left;transition:transform .4s ease}
.ob-segs [data-state="done"]::after{transform:none}
.ob-segs [data-state="current"]::after{transform:none;background:linear-gradient(90deg,var(--ob-accent) 55%,var(--ob-pink))}
/* B: labelled <progress> with a % figure, and a row of step names under it */
.ob-lg{padding:40px 44px 36px}
.ob-lg .ob-brand{justify-content:center}
.ob-lg .ob-title{font-size:1.35rem;text-align:center;margin-top:14px}
.ob-meter{display:flex;justify-content:space-between;align-items:baseline;margin-top:26px;font-size:.85rem;font-weight:600;color:var(--ob-muted)}
.ob-meter span{color:var(--ob-dark);font-weight:700;font-variant-numeric:tabular-nums}
.ob-progress{display:block;width:100%;height:10px;margin-top:8px;border:0;border-radius:99px;background:var(--ob-track);overflow:hidden;-webkit-appearance:none;appearance:none}
.ob-progress::-webkit-progress-bar{background:var(--ob-track);border-radius:99px}
.ob-progress::-webkit-progress-value{background:linear-gradient(90deg,var(--ob-accent),var(--ob-pink));border-radius:99px;transition:width .4s ease}
.ob-progress::-moz-progress-bar{background:linear-gradient(90deg,var(--ob-accent),var(--ob-pink));border-radius:99px}
.ob-names{list-style:none;margin:12px 0 0;padding:0;display:grid;grid-template-columns:repeat(5,1fr);gap:4px}
.ob-names li{display:flex;flex-direction:column;align-items:center;gap:5px;font-size:.78rem;color:var(--ob-muted);text-align:center}
.ob-mark{width:16px;height:16px;border-radius:50%;border:2px solid var(--ob-field);background:#fff;display:grid;place-items:center;color:#fff;transition:background .25s,border-color .25s}
.ob-mark svg{display:none;width:10px;height:10px;stroke-width:3.5}
.ob-names [data-state="current"]{color:var(--ob-ink);font-weight:700}
.ob-names [data-state="current"] .ob-mark{border-color:var(--ob-pink);background:var(--ob-pink);box-shadow:0 0 0 4px #fce7f3}
.ob-names [data-state="done"]{color:var(--ob-dark);font-weight:600}
.ob-names [data-state="done"] .ob-mark{border-color:var(--ob-accent);background:var(--ob-accent)}
.ob-names [data-state="done"] .ob-mark svg{display:block}
/* step panels */
.ob-panel{margin-top:26px;display:grid;gap:18px;animation:ob-in .3s ease}
@keyframes ob-in{from{opacity:0;transform:translateY(6px)}}
.ob-h{margin:0;font-size:1.45rem;font-weight:700;letter-spacing:-.02em;line-height:1.25}
.ob-h:focus{outline:none}
.ob-kick{display:block;font-size:.72rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--ob-dark);margin-bottom:4px}
.ob-lead{margin:-10px 0 0;color:var(--ob-muted);font-size:.95rem}
.ob-lg .ob-panel{justify-items:center;text-align:center;margin-top:30px}
.ob-lg .ob-h{font-size:1.3rem}
.ob-icon{width:56px;height:56px;border-radius:16px;display:grid;place-items:center;background:var(--ob-soft);color:var(--ob-dark)}
.ob-icon svg{width:26px;height:26px}
.ob-col{width:100%;max-width:440px;text-align:left}
.ob-field{display:grid;gap:6px;min-width:0}
.ob-field > label,.ob-legend{font-size:.88rem;font-weight:600}
fieldset.ob-field{border:0;margin:0;padding:0}
.ob-legend{padding:0;margin-bottom:8px}
.ob-hint{font-size:.82rem;color:var(--ob-muted);margin:4px 0 0}
.ob-hint b{color:var(--ob-dark)}
.ob-input{width:100%;font:inherit;font-size:.95rem;color:var(--ob-ink);background:#fff;border:1.5px solid var(--ob-field);border-radius:12px;padding:10px 14px;min-height:46px}
.ob-input:focus-visible{outline:none;border-color:var(--ob-accent);box-shadow:0 0 0 4px var(--ob-soft)}
.ob-input[readonly]{background:var(--ob-tint);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.86rem}
/* chips and pills: the checkbox/radio sits invisibly over its label, the label follows :has(:checked) */
.ob-chips{display:flex;flex-wrap:wrap;gap:10px}
.ob-lg .ob-chips{justify-content:center}
.ob-chip{position:relative;display:inline-flex;align-items:center;gap:7px;padding:9px 16px 9px 13px;border:1.5px solid var(--ob-field);border-radius:999px;background:#fff;font-size:.92rem;font-weight:500;cursor:pointer;transition:background .2s,border-color .2s,color .2s}
.ob-chip input{position:absolute;inset:0;width:100%;height:100%;margin:0;opacity:0;cursor:pointer}
.ob-chip svg{width:16px;height:16px;stroke-width:2.5}
.ob-chip .tick,.ob-chip:has(:checked) .plus{display:none}
.ob-chip:has(:checked) .tick{display:block}
.ob-chip:hover{border-color:var(--ob-accent)}
.ob-chip:has(:checked){background:var(--ob-accent);border-color:var(--ob-accent);color:#fff}
.ob-chip:has(:focus-visible){outline:2px solid var(--ob-accent);outline-offset:2px}
.ob-chip.day{padding:7px 12px;font-size:.85rem;min-width:52px;justify-content:center}
.ob-chip.pill{padding:10px 18px}
/* A step 2: goal cards with a visible radio */
.ob-goals{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:12px}
.ob-goal{position:relative;display:grid;gap:2px;align-content:start;padding:16px 16px 18px;border:1.5px solid var(--ob-field);border-radius:16px;cursor:pointer;transition:border-color .2s,background .2s,box-shadow .2s}
.ob-goal input{position:absolute;top:16px;right:16px;width:18px;height:18px;margin:0;accent-color:var(--ob-accent)}
.ob-goal input:focus-visible{outline:2px solid var(--ob-accent);outline-offset:3px}
.ob-goal b{font-size:2rem;line-height:1;font-weight:800;letter-spacing:-.03em;color:var(--ob-dark)}
.ob-goal .unit{font-size:.85rem;font-weight:600}
.ob-goal .line{font-size:.85rem;color:var(--ob-muted);margin-top:6px}
.ob-goal:has(:checked){border-color:var(--ob-accent);background:var(--ob-tint);box-shadow:0 0 0 3px var(--ob-soft)}
.ob-tag{justify-self:start;margin-bottom:8px;padding:2px 8px;border-radius:99px;background:var(--ob-pink);color:#fff;font-size:.68rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase}
.ob-time{max-width:220px}
/* A step 4: people with Follow toggle buttons (aria-pressed) */
.ob-people{list-style:none;margin:0;padding:0;display:grid;border:1px solid #e7e2f3;border-radius:16px}
.ob-person{display:flex;align-items:center;gap:14px;padding:14px 16px}
.ob-person + .ob-person{border-top:1px solid #e7e2f3}
.ob-avatar{width:44px;height:44px;flex:none;border-radius:50%;display:grid;place-items:center;font-size:.9rem;font-weight:700}
.ob-who{flex:1;min-width:0;display:grid;font-size:.95rem;font-weight:600}
.ob-who span{font-size:.82rem;font-weight:400;color:var(--ob-muted)}
.ob-follow{flex:none;display:inline-flex;align-items:center;gap:6px;min-height:38px;padding:0 16px;border-radius:999px;border:1.5px solid var(--ob-accent);background:#fff;color:var(--ob-dark);font:inherit;font-size:.88rem;font-weight:600;cursor:pointer;transition:background .2s,color .2s}
.ob-follow svg{display:none;width:15px;height:15px;stroke-width:3}
.ob-follow:hover{background:var(--ob-soft)}
.ob-follow[aria-pressed="true"]{background:var(--ob-accent);color:#fff}
.ob-follow[aria-pressed="true"]:hover{background:var(--ob-dark);border-color:var(--ob-dark)}
.ob-follow[aria-pressed="true"] svg{display:block}
.ob-follow:focus-visible{outline:none;box-shadow:0 0 0 3px #fff,0 0 0 5px var(--ob-accent)}
/* B step 5: read-only link + Copy, with a polite status line */
.ob-copy{display:flex;gap:8px}
.ob-copy .ob-input{flex:1;min-width:0}
.ob-status{margin:8px 0 0;min-height:1.4em;font-size:.85rem;font-weight:600;color:var(--ob-dark)}
/* Back / Continue bar */
.ob-nav{display:flex;gap:10px;margin-top:28px;padding-top:20px;border-top:1px solid #ece8f6}
.ob-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:46px;padding:0 24px;border-radius:999px;border:1.5px solid var(--ob-accent);background:var(--ob-accent);color:#fff;font:inherit;font-size:.95rem;font-weight:600;cursor:pointer;transition:background .2s,box-shadow .2s}
.ob-btn:hover{background:var(--ob-dark)}
.ob-btn:focus-visible{outline:none;box-shadow:0 0 0 3px #fff,0 0 0 5px var(--ob-accent)}
.ob-btn.ghost{background:#fff;color:var(--ob-dark);border-color:var(--ob-field)}
.ob-btn.ghost:hover{background:var(--ob-tint)}
.ob-btn svg{width:18px;height:18px}
.ob-nav .ob-btn:not(.ghost){margin-left:auto}
.ob-done{justify-items:center;text-align:center;padding:10px 0 4px}
.ob-done .ob-icon{border-radius:50%;background:#fce7f3;color:#be185d}
.ob-done .ob-lead{margin:0;max-width:440px}
@media (prefers-reduced-motion:reduce){.ob *,.ob *::before,.ob *::after{transition:none!important;animation:none!important}.ob-progress::-webkit-progress-value{transition:none}}
@media (max-width:600px){.ob,.ob-lg{padding:24px 18px}.ob-h,.ob-lg .ob-h{font-size:1.25rem}.ob-names li{font-size:.7rem}.ob-person{flex-wrap:wrap}.ob-who{flex-basis:calc(100% - 58px)}.ob-follow{margin-left:58px}}
JavaScript
// Onboarding wizard: one step at a time, stepper marked with data-state and aria-current="step", focus on the new heading.
// Also keeps "Step 2 of 4", the <progress> value and its % text in sync, and handles Skip, pick-at-least-N groups, toggles and Copy.
document.querySelectorAll('[data-wizard]').forEach(function (wz) {
var panels = wz.querySelectorAll('[data-step]'), steps = wz.querySelectorAll('[data-stepper] > li');
var back = wz.querySelector('[data-back]'), next = wz.querySelector('[data-next]'), skip = wz.querySelector('[data-skip]');
var submit = wz.querySelector('[type="submit"]'), done = wz.querySelector('[data-done]'), at = 0;
var bar = wz.querySelector('[data-progress]'), pct = wz.querySelector('[data-pct]'), pos = wz.querySelector('[data-pos]');
function mark(i) {
steps.forEach(function (s, n) {
s.dataset.state = n < i ? 'done' : n === i ? 'current' : 'next';
if (n === i) s.setAttribute('aria-current', 'step'); else s.removeAttribute('aria-current');
s.querySelector('[data-sr]').textContent = n < i ? ', completed' : '';
});
var p = Math.round(Math.min(i + 1, steps.length) / steps.length * 100);
if (bar) bar.value = p;
if (pct) pct.textContent = p + '%';
if (pos) pos.textContent = i < steps.length ? 'Step ' + (i + 1) + ' of ' + steps.length : 'All done';
}
function picks() { // [data-min] groups: fewer than N ticked boxes puts a custom error on the first box
wz.querySelectorAll('[data-min]').forEach(function (g) {
var boxes = g.querySelectorAll('input[type="checkbox"]'), n = g.querySelectorAll(':checked').length, min = +g.dataset.min, c = g.querySelector('[data-count]');
boxes[0].setCustomValidity(n < min ? 'Pick at least ' + min + ' (' + n + ' picked so far).' : '');
if (c) c.textContent = n + ' picked';
});
}
function go(i, focus) {
at = i; mark(i); picks();
panels.forEach(function (p, n) { p.hidden = n !== i; });
back.hidden = i === 0; next.hidden = i === panels.length - 1; submit.hidden = !next.hidden;
if (skip) skip.hidden = !panels[i].hasAttribute('data-optional');
wz.querySelectorAll('[data-out]').forEach(function (o) {
var f = wz.elements[o.dataset.out], v = f && (f.length && !f.options ? [].filter.call(f, function (x) { return x.checked; }).map(function (x) { return x.value; }).join(', ') : f.value);
o.textContent = v || 'None';
});
if (focus) panels[i].querySelector('[tabindex="-1"]').focus();
}
function valid() {
var bad = [].find.call(panels[at].querySelectorAll('input, select, textarea'), function (f) { return !f.checkValidity(); });
if (bad) bad.reportValidity();
return !bad;
}
function finish() {
// Last step: send the form here (fetch or wz.submit()). The demo shows the done panel instead.
panels[at].hidden = true; back.parentNode.hidden = true; if (skip) skip.hidden = true; mark(steps.length);
done.hidden = false; done.querySelector('[tabindex="-1"]').focus();
}
wz.noValidate = true;
back.addEventListener('click', function () { go(at - 1, true); });
next.addEventListener('click', function () { if (valid()) go(at + 1, true); });
if (skip) skip.addEventListener('click', function () { if (at < panels.length - 1) go(at + 1, true); else finish(); });
wz.addEventListener('change', picks);
wz.addEventListener('submit', function (e) {
e.preventDefault();
if (!valid()) return;
if (at < panels.length - 1) return go(at + 1, true);
finish();
});
wz.querySelectorAll('[aria-pressed]').forEach(function (b) {
b.addEventListener('click', function () { b.setAttribute('aria-pressed', b.getAttribute('aria-pressed') !== 'true'); });
});
wz.querySelectorAll('[data-copy]').forEach(function (b) {
var f = document.getElementById(b.dataset.copy), say = wz.querySelector('[data-status]');
function tell(t) { if (say) { say.textContent = t; clearTimeout(say.t); say.t = setTimeout(function () { say.textContent = ''; }, 4000); } }
b.addEventListener('click', function () {
(navigator.clipboard ? navigator.clipboard.writeText(f.value) : Promise.reject()).then(function () { tell('Link copied'); },
function () { f.select(); tell('Link selected: press Ctrl+C or Cmd+C to copy'); });
});
});
if (done) done.querySelector('[data-restart]').addEventListener('click', function () {
wz.reset(); wz.querySelectorAll('[aria-pressed]').forEach(function (b) { b.setAttribute('aria-pressed', 'false'); });
done.hidden = true; back.parentNode.hidden = false; go(0, true);
});
go(0);
});
/* Onboarding wizards on Bootstrap 5: .card, .form-control, .form-select, .btn-check + .btn chips, .form-check-input,
.list-group and .btn, restyled through Bootstrap's CSS variables. The segments, the <progress> bar and the step names
are the custom parts. The script hides every step but the current one, sets data-state and aria-current="step" on the
<ol data-stepper>, fills the segments or the bar and moves focus to the new step's heading. Without JavaScript every step shows. */
.ob{--ob-accent:#6d28d9;--ob-dark:#5b21b6;--ob-soft:#ede9fe;--ob-pink:#db2777;--ob-ink:#1e1b2e;--ob-muted:#5c5670;--ob-field:#8a82a3;--ob-track:#e4ddf8;--ob-tint:#faf8ff;
--bs-border-color:#8a82a3;--bs-body-color:#1e1b2e;--bs-secondary-color:#5c5670;
border:0;border-radius:20px;box-shadow:0 1px 2px rgba(30,27,46,.06),0 16px 40px -18px rgba(76,29,149,.3)}
.ob > .card-body{padding:28px 32px 30px}
.ob.ob-lg > .card-body{padding:40px 44px 36px}
.ob svg{fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.ob .form-control,.ob .form-select{border-width:1.5px;border-radius:12px;min-height:46px;padding:10px 14px;font-size:.95rem;color:var(--ob-ink)}
.ob .form-select{padding-right:38px}
.ob .form-control:focus,.ob .form-select:focus{border-color:var(--ob-accent);box-shadow:0 0 0 4px var(--ob-soft)}
.ob .form-control[readonly]{background:var(--ob-tint);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.86rem}
.ob .form-label,.ob legend{font-size:.88rem;font-weight:600;margin-bottom:6px}
.ob .form-check-input{border-width:1.5px;border-color:var(--ob-field)}
.ob .form-check-input:checked{background-color:var(--ob-accent);border-color:var(--ob-accent)}
.ob .form-check-input:focus{border-color:var(--ob-accent);box-shadow:0 0 0 3px var(--ob-soft)}
.ob .form-check-input:focus-visible{outline:2px solid var(--ob-accent);outline-offset:3px}
.ob .btn{--bs-btn-padding-x:24px;--bs-btn-font-weight:600;--bs-btn-font-size:.95rem;--bs-btn-border-width:1.5px;--bs-btn-border-radius:999px;min-height:46px;display:inline-flex;align-items:center;justify-content:center;gap:8px}
.ob .btn-primary{--bs-btn-bg:var(--ob-accent);--bs-btn-border-color:var(--ob-accent);--bs-btn-hover-bg:var(--ob-dark);--bs-btn-hover-border-color:var(--ob-dark);--bs-btn-active-bg:var(--ob-dark);--bs-btn-active-border-color:var(--ob-dark)}
.ob .btn-outline-secondary{--bs-btn-bg:#fff;--bs-btn-color:var(--ob-dark);--bs-btn-border-color:var(--ob-field);--bs-btn-hover-bg:var(--ob-tint);--bs-btn-hover-color:var(--ob-dark);--bs-btn-hover-border-color:var(--ob-field);--bs-btn-active-bg:var(--ob-soft);--bs-btn-active-color:var(--ob-dark);--bs-btn-active-border-color:var(--ob-field)}
.ob .btn:focus-visible{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--ob-accent);outline:0}
.ob .btn svg{width:18px;height:18px}
.ob-brand{display:flex;align-items:center;gap:10px}
.ob-logo{width:34px;height:34px;flex:none;border-radius:10px;display:grid;place-items:center;background:var(--ob-accent);color:#fff}
.ob-logo svg{width:20px;height:20px}
.ob-title{font-size:1.05rem;letter-spacing:-.01em;line-height:1.3}
/* A: "Step 2 of 4" + Skip, then one bar segment per step (the segments are the stepper <li>s, names visually hidden) */
.ob-top{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:24px;min-height:34px}
.ob-pos{font-size:.85rem;font-weight:700;color:var(--ob-dark)}
.ob .ob-skip{--bs-btn-padding-x:10px;--bs-btn-padding-y:6px;--bs-btn-font-size:.88rem;--bs-btn-color:var(--ob-dark);--bs-btn-hover-color:var(--ob-dark);--bs-btn-hover-bg:var(--ob-soft);--bs-btn-active-color:var(--ob-dark);--bs-btn-border-radius:8px;--bs-btn-border-width:0;min-height:0;margin-right:-10px;text-underline-offset:3px}
.ob .ob-skip:focus-visible{box-shadow:none;outline:2px solid var(--ob-accent);outline-offset:1px}
.ob-segs{list-style:none;margin:8px 0 0;padding:0;display:grid;grid-template-columns:repeat(4,1fr);gap:6px}
.ob-segs li{position:relative;height:6px;border-radius:99px;background:var(--ob-track);overflow:hidden}
.ob-segs li::after{content:"";position:absolute;inset:0;border-radius:inherit;background:var(--ob-accent);transform:scaleX(0);transform-origin:left;transition:transform .4s ease}
.ob-segs [data-state="done"]::after{transform:none}
.ob-segs [data-state="current"]::after{transform:none;background:linear-gradient(90deg,var(--ob-accent) 55%,var(--ob-pink))}
/* B: labelled <progress> with a % figure, and a row of step names under it */
.ob-lg .ob-brand{justify-content:center}
.ob-lg .ob-title{font-size:1.35rem;text-align:center;margin-top:14px}
.ob-meter{display:flex;justify-content:space-between;align-items:baseline;margin-top:26px;font-size:.85rem;font-weight:600;color:var(--ob-muted)}
.ob-meter span{color:var(--ob-dark);font-weight:700;font-variant-numeric:tabular-nums}
.ob-progress{display:block;width:100%;height:10px;margin-top:8px;border:0;border-radius:99px;background:var(--ob-track);overflow:hidden;-webkit-appearance:none;appearance:none}
.ob-progress::-webkit-progress-bar{background:var(--ob-track);border-radius:99px}
.ob-progress::-webkit-progress-value{background:linear-gradient(90deg,var(--ob-accent),var(--ob-pink));border-radius:99px;transition:width .4s ease}
.ob-progress::-moz-progress-bar{background:linear-gradient(90deg,var(--ob-accent),var(--ob-pink));border-radius:99px}
.ob-names{list-style:none;margin:12px 0 0;padding:0;display:grid;grid-template-columns:repeat(5,1fr);gap:4px}
.ob-names li{display:flex;flex-direction:column;align-items:center;gap:5px;font-size:.78rem;color:var(--ob-muted);text-align:center}
.ob-mark{width:16px;height:16px;border-radius:50%;border:2px solid var(--ob-field);background:#fff;display:grid;place-items:center;color:#fff;transition:background .25s,border-color .25s}
.ob-mark svg{display:none;width:10px;height:10px;stroke-width:3.5}
.ob-names [data-state="current"]{color:var(--ob-ink);font-weight:700}
.ob-names [data-state="current"] .ob-mark{border-color:var(--ob-pink);background:var(--ob-pink);box-shadow:0 0 0 4px #fce7f3}
.ob-names [data-state="done"]{color:var(--ob-dark);font-weight:600}
.ob-names [data-state="done"] .ob-mark{border-color:var(--ob-accent);background:var(--ob-accent)}
.ob-names [data-state="done"] .ob-mark svg{display:block}
/* step panels */
.ob-panel{margin-top:26px;display:grid;gap:18px;animation:ob-in .3s ease}
@keyframes ob-in{from{opacity:0;transform:translateY(6px)}}
.ob-h{font-size:1.45rem;letter-spacing:-.02em;line-height:1.25}
.ob-h:focus{outline:none}
.ob-kick{display:block;font-size:.72rem;letter-spacing:.1em;text-transform:uppercase;color:var(--ob-dark);margin-bottom:4px}
.ob-lead{margin:-10px 0 0;color:var(--ob-muted);font-size:.95rem}
.ob-lg .ob-panel{justify-items:center;text-align:center;margin-top:30px}
.ob-lg .ob-h{font-size:1.3rem}
.ob-icon{width:56px;height:56px;border-radius:16px;display:grid;place-items:center;background:var(--ob-soft);color:var(--ob-dark)}
.ob-icon svg{width:26px;height:26px}
.ob-col{width:100%;max-width:440px;text-align:left}
.ob-hint{font-size:.82rem;color:var(--ob-muted);margin:10px 0 0}
.ob-hint b{color:var(--ob-dark)}
/* chips and pills: .btn-check + .btn, the label turns violet when its box is checked */
.ob-chips{display:flex;flex-wrap:wrap;gap:10px}
.ob-lg .ob-chips{justify-content:center}
.ob .ob-chip{--bs-btn-padding-x:16px;--bs-btn-padding-y:9px;--bs-btn-font-size:.92rem;--bs-btn-font-weight:500;--bs-btn-color:var(--ob-ink);--bs-btn-bg:#fff;--bs-btn-border-color:var(--ob-field);
--bs-btn-hover-color:var(--ob-ink);--bs-btn-hover-bg:#fff;--bs-btn-hover-border-color:var(--ob-accent);--bs-btn-active-color:#fff;--bs-btn-active-bg:var(--ob-accent);--bs-btn-active-border-color:var(--ob-accent);
min-height:0;gap:7px;padding-left:13px;line-height:1.5}
.ob .btn-check:focus-visible + .ob-chip{box-shadow:none;outline:2px solid var(--ob-accent);outline-offset:2px}
.ob .ob-chip svg{width:16px;height:16px;stroke-width:2.5}
.ob-chip .tick,.btn-check:checked + .ob-chip .plus{display:none}
.btn-check:checked + .ob-chip .tick{display:block}
.ob .ob-chip.day{--bs-btn-padding-x:12px;--bs-btn-padding-y:7px;--bs-btn-font-size:.85rem;min-width:52px}
.ob .ob-chip.pill{--bs-btn-padding-x:18px;--bs-btn-padding-y:10px}
/* A step 2: goal cards with a visible .form-check-input radio, the card follows :has(:checked) */
.ob-goals{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:12px}
.ob-goal{position:relative;display:grid;gap:2px;align-content:start;padding:16px 16px 18px;border:1.5px solid var(--ob-field);border-radius:16px;cursor:pointer;transition:border-color .2s,background .2s,box-shadow .2s}
.ob-goal .form-check-input{position:absolute;top:16px;right:16px;width:18px;height:18px;margin:0}
.ob-goal b{font-size:2rem;line-height:1;font-weight:800;letter-spacing:-.03em;color:var(--ob-dark)}
.ob-goal .unit{font-size:.85rem;font-weight:600}
.ob-goal .line{font-size:.85rem;color:var(--ob-muted);margin-top:6px}
.ob-goal:has(:checked){border-color:var(--ob-accent);background:var(--ob-tint);box-shadow:0 0 0 3px var(--ob-soft)}
.ob-tag{justify-self:start;margin-bottom:8px;padding:2px 8px;border-radius:99px;background:var(--ob-pink);color:#fff;font-size:.68rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase}
.ob-time{max-width:220px}
/* A step 4: .list-group of people with Follow toggle buttons (aria-pressed) */
.ob .list-group{--bs-list-group-border-color:#e7e2f3;--bs-list-group-border-radius:16px;--bs-list-group-item-padding-x:16px;--bs-list-group-item-padding-y:14px}
.ob .list-group-item{gap:14px}
.ob-avatar{width:44px;height:44px;flex:none;border-radius:50%;display:grid;place-items:center;font-size:.9rem;font-weight:700}
.ob-who{flex:1;min-width:0;display:grid;font-size:.95rem;font-weight:600}
.ob-who span{font-size:.82rem;font-weight:400;color:var(--ob-muted)}
.ob .ob-follow{--bs-btn-padding-x:16px;--bs-btn-font-size:.88rem;--bs-btn-color:var(--ob-dark);--bs-btn-bg:#fff;--bs-btn-border-color:var(--ob-accent);--bs-btn-hover-color:var(--ob-dark);--bs-btn-hover-bg:var(--ob-soft);--bs-btn-hover-border-color:var(--ob-accent);
--bs-btn-active-color:#fff;--bs-btn-active-bg:var(--ob-accent);--bs-btn-active-border-color:var(--ob-accent);flex:none;min-height:38px;gap:6px}
.ob .ob-follow svg{display:none;width:15px;height:15px;stroke-width:3}
.ob .ob-follow[aria-pressed="true"]{--bs-btn-bg:var(--ob-accent);--bs-btn-color:#fff;--bs-btn-hover-bg:var(--ob-dark);--bs-btn-hover-color:#fff;--bs-btn-hover-border-color:var(--ob-dark)}
.ob .ob-follow[aria-pressed="true"] svg{display:block}
/* B step 5: read-only link + Copy, with a polite status line */
.ob-status{margin:14px 0 0;min-height:1.4em;font-size:.85rem;font-weight:600;color:var(--ob-dark)}
/* Back / Continue bar */
.ob-nav{display:flex;gap:10px;margin-top:28px;padding-top:20px;border-top:1px solid #ece8f6}
.ob-nav .btn-primary{margin-left:auto}
.ob-done{justify-items:center;text-align:center;padding:10px 0 4px}
.ob-done .ob-icon{border-radius:50%;background:#fce7f3;color:#be185d}
.ob-done .ob-lead{margin:0;max-width:440px}
@media (prefers-reduced-motion:reduce){.ob *,.ob *::before,.ob *::after{transition:none!important;animation:none!important}.ob-progress::-webkit-progress-value{transition:none}}
@media (max-width:600px){.ob > .card-body,.ob.ob-lg > .card-body{padding:24px 18px}.ob-h{font-size:1.25rem}.ob-lg .ob-h{font-size:1.25rem}.ob-names li{font-size:.7rem}
.ob .list-group-item{flex-wrap:wrap}.ob-who{flex-basis:calc(100% - 58px)}.ob .ob-follow{margin-left:58px}}
JavaScript
// Onboarding wizard: one step at a time, stepper marked with data-state and aria-current="step", focus on the new heading.
// Also keeps "Step 2 of 4", the <progress> value and its % text in sync, and handles Skip, pick-at-least-N groups, toggles and Copy.
document.querySelectorAll('[data-wizard]').forEach(function (wz) {
var panels = wz.querySelectorAll('[data-step]'), steps = wz.querySelectorAll('[data-stepper] > li');
var back = wz.querySelector('[data-back]'), next = wz.querySelector('[data-next]'), skip = wz.querySelector('[data-skip]');
var submit = wz.querySelector('[type="submit"]'), done = wz.querySelector('[data-done]'), at = 0;
var bar = wz.querySelector('[data-progress]'), pct = wz.querySelector('[data-pct]'), pos = wz.querySelector('[data-pos]');
function mark(i) {
steps.forEach(function (s, n) {
s.dataset.state = n < i ? 'done' : n === i ? 'current' : 'next';
if (n === i) s.setAttribute('aria-current', 'step'); else s.removeAttribute('aria-current');
s.querySelector('[data-sr]').textContent = n < i ? ', completed' : '';
});
var p = Math.round(Math.min(i + 1, steps.length) / steps.length * 100);
if (bar) bar.value = p;
if (pct) pct.textContent = p + '%';
if (pos) pos.textContent = i < steps.length ? 'Step ' + (i + 1) + ' of ' + steps.length : 'All done';
}
function picks() { // [data-min] groups: fewer than N ticked boxes puts a custom error on the first box
wz.querySelectorAll('[data-min]').forEach(function (g) {
var boxes = g.querySelectorAll('input[type="checkbox"]'), n = g.querySelectorAll(':checked').length, min = +g.dataset.min, c = g.querySelector('[data-count]');
boxes[0].setCustomValidity(n < min ? 'Pick at least ' + min + ' (' + n + ' picked so far).' : '');
if (c) c.textContent = n + ' picked';
});
}
function go(i, focus) {
at = i; mark(i); picks();
panels.forEach(function (p, n) { p.hidden = n !== i; });
back.hidden = i === 0; next.hidden = i === panels.length - 1; submit.hidden = !next.hidden;
if (skip) skip.hidden = !panels[i].hasAttribute('data-optional');
wz.querySelectorAll('[data-out]').forEach(function (o) {
var f = wz.elements[o.dataset.out], v = f && (f.length && !f.options ? [].filter.call(f, function (x) { return x.checked; }).map(function (x) { return x.value; }).join(', ') : f.value);
o.textContent = v || 'None';
});
if (focus) panels[i].querySelector('[tabindex="-1"]').focus();
}
function valid() {
var bad = [].find.call(panels[at].querySelectorAll('input, select, textarea'), function (f) { return !f.checkValidity(); });
if (bad) bad.reportValidity();
return !bad;
}
function finish() {
// Last step: send the form here (fetch or wz.submit()). The demo shows the done panel instead.
panels[at].hidden = true; back.parentNode.hidden = true; if (skip) skip.hidden = true; mark(steps.length);
done.hidden = false; done.querySelector('[tabindex="-1"]').focus();
}
wz.noValidate = true;
back.addEventListener('click', function () { go(at - 1, true); });
next.addEventListener('click', function () { if (valid()) go(at + 1, true); });
if (skip) skip.addEventListener('click', function () { if (at < panels.length - 1) go(at + 1, true); else finish(); });
wz.addEventListener('change', picks);
wz.addEventListener('submit', function (e) {
e.preventDefault();
if (!valid()) return;
if (at < panels.length - 1) return go(at + 1, true);
finish();
});
wz.querySelectorAll('[aria-pressed]').forEach(function (b) {
b.addEventListener('click', function () { b.setAttribute('aria-pressed', b.getAttribute('aria-pressed') !== 'true'); });
});
wz.querySelectorAll('[data-copy]').forEach(function (b) {
var f = document.getElementById(b.dataset.copy), say = wz.querySelector('[data-status]');
function tell(t) { if (say) { say.textContent = t; clearTimeout(say.t); say.t = setTimeout(function () { say.textContent = ''; }, 4000); } }
b.addEventListener('click', function () {
(navigator.clipboard ? navigator.clipboard.writeText(f.value) : Promise.reject()).then(function () { tell('Link copied'); },
function () { f.select(); tell('Link selected: press Ctrl+C or Cmd+C to copy'); });
});
});
if (done) done.querySelector('[data-restart]').addEventListener('click', function () {
wz.reset(); wz.querySelectorAll('[aria-pressed]').forEach(function (b) { b.setAttribute('aria-pressed', 'false'); });
done.hidden = true; back.parentNode.hidden = false; go(0, true);
});
go(0);
});
// Onboarding wizard: one step at a time, stepper marked with data-state and aria-current="step", focus on the new heading.
// Also keeps "Step 2 of 4", the <progress> value and its % text in sync, and handles Skip, pick-at-least-N groups, toggles and Copy.
document.querySelectorAll('[data-wizard]').forEach(function (wz) {
var panels = wz.querySelectorAll('[data-step]'), steps = wz.querySelectorAll('[data-stepper] > li');
var back = wz.querySelector('[data-back]'), next = wz.querySelector('[data-next]'), skip = wz.querySelector('[data-skip]');
var submit = wz.querySelector('[type="submit"]'), done = wz.querySelector('[data-done]'), at = 0;
var bar = wz.querySelector('[data-progress]'), pct = wz.querySelector('[data-pct]'), pos = wz.querySelector('[data-pos]');
function mark(i) {
steps.forEach(function (s, n) {
s.dataset.state = n < i ? 'done' : n === i ? 'current' : 'next';
if (n === i) s.setAttribute('aria-current', 'step'); else s.removeAttribute('aria-current');
s.querySelector('[data-sr]').textContent = n < i ? ', completed' : '';
});
var p = Math.round(Math.min(i + 1, steps.length) / steps.length * 100);
if (bar) bar.value = p;
if (pct) pct.textContent = p + '%';
if (pos) pos.textContent = i < steps.length ? 'Step ' + (i + 1) + ' of ' + steps.length : 'All done';
}
function picks() { // [data-min] groups: fewer than N ticked boxes puts a custom error on the first box
wz.querySelectorAll('[data-min]').forEach(function (g) {
var boxes = g.querySelectorAll('input[type="checkbox"]'), n = g.querySelectorAll(':checked').length, min = +g.dataset.min, c = g.querySelector('[data-count]');
boxes[0].setCustomValidity(n < min ? 'Pick at least ' + min + ' (' + n + ' picked so far).' : '');
if (c) c.textContent = n + ' picked';
});
}
function go(i, focus) {
at = i; mark(i); picks();
panels.forEach(function (p, n) { p.hidden = n !== i; });
back.hidden = i === 0; next.hidden = i === panels.length - 1; submit.hidden = !next.hidden;
if (skip) skip.hidden = !panels[i].hasAttribute('data-optional');
wz.querySelectorAll('[data-out]').forEach(function (o) {
var f = wz.elements[o.dataset.out], v = f && (f.length && !f.options ? [].filter.call(f, function (x) { return x.checked; }).map(function (x) { return x.value; }).join(', ') : f.value);
o.textContent = v || 'None';
});
if (focus) panels[i].querySelector('[tabindex="-1"]').focus();
}
function valid() {
var bad = [].find.call(panels[at].querySelectorAll('input, select, textarea'), function (f) { return !f.checkValidity(); });
if (bad) bad.reportValidity();
return !bad;
}
function finish() {
// Last step: send the form here (fetch or wz.submit()). The demo shows the done panel instead.
panels[at].hidden = true; back.parentNode.hidden = true; if (skip) skip.hidden = true; mark(steps.length);
done.hidden = false; done.querySelector('[tabindex="-1"]').focus();
}
wz.noValidate = true;
back.addEventListener('click', function () { go(at - 1, true); });
next.addEventListener('click', function () { if (valid()) go(at + 1, true); });
if (skip) skip.addEventListener('click', function () { if (at < panels.length - 1) go(at + 1, true); else finish(); });
wz.addEventListener('change', picks);
wz.addEventListener('submit', function (e) {
e.preventDefault();
if (!valid()) return;
if (at < panels.length - 1) return go(at + 1, true);
finish();
});
wz.querySelectorAll('[aria-pressed]').forEach(function (b) {
b.addEventListener('click', function () { b.setAttribute('aria-pressed', b.getAttribute('aria-pressed') !== 'true'); });
});
wz.querySelectorAll('[data-copy]').forEach(function (b) {
var f = document.getElementById(b.dataset.copy), say = wz.querySelector('[data-status]');
function tell(t) { if (say) { say.textContent = t; clearTimeout(say.t); say.t = setTimeout(function () { say.textContent = ''; }, 4000); } }
b.addEventListener('click', function () {
(navigator.clipboard ? navigator.clipboard.writeText(f.value) : Promise.reject()).then(function () { tell('Link copied'); },
function () { f.select(); tell('Link selected: press Ctrl+C or Cmd+C to copy'); });
});
});
if (done) done.querySelector('[data-restart]').addEventListener('click', function () {
wz.reset(); wz.querySelectorAll('[aria-pressed]').forEach(function (b) { b.setAttribute('aria-pressed', 'false'); });
done.hidden = true; back.parentNode.hidden = false; go(0, true);
});
go(0);
});
About this set
Two first-run onboarding flows where progress is shown as a bar rather than numbered steps. Segmented progress is a four-step setup for a reading app: pick at least three topics from checkbox chips, choose a daily goal from radio cards, set a reminder time and weekdays, then follow suggested readers with toggle buttons. A Step 2 of 4 line and a Skip button sit above four segments that fill as you advance. Skip appears only on steps marked optional and moves on without validating. Progress bar with percentage is a five-step team profile with an icon per step, centred in a larger card: team name, team size pills, role select, tool chips and an invite link with a Copy button. A native progress element with a visible label shows the percentage, and a row of step names under it carries aria-current. Use the segments for four or five quick taste questions and the percentage bar when a setup has more steps and people want to know how far along they are. The shared vanilla script validates each step with constraint validation, enforces the minimum of three topics through setCustomValidity, keeps the bar and text in sync and moves focus to each new heading. Without JavaScript every step shows and the form submits.
What’s included
Segmented bar built from the stepper list itself
Native progress element with a label and a live percentage figure
Skip button that appears only on optional steps
Pick-at-least-three rule enforced with setCustomValidity and a running count
Follow toggle buttons with aria-pressed and a Copy button with a polite status message
Every step visible and submittable without JavaScript
Accessibility
Each stepper is an ordered list labelled Progress with aria-current="step" on the current item and a visually hidden ", completed" on finished ones; the progress element is labelled Setup progress. Focus moves to each new step heading, which starts with its Step 2 of 4 kicker, and chips, cards, toggles and buttons show visible focus rings. Follow buttons expose their state with aria-pressed, the copy result is announced through role="status", and animations and transitions stop under prefers-reduced-motion.
Customise it
Change --ob-accent, --ob-dark, --ob-soft, --ob-pink and --ob-track on .ob in the plain CSS and Bootstrap versions; Bootstrap buttons, chips and fields read them through --bs-btn-* and --bs-border-color. In Tailwind edit the --color-ob-* theme colours.