Onboarding — Profile/setup completion nudge bar
A slim profile-completion nudge that keeps new users moving through setup by pairing a live progress bar with the single next best action. It announces how complete the profile is, suggests the next step like adding a photo or inviting a teammate, and advances the percentage as each step is finished. When everything is done the bar flips to a celebratory success state that can be cleared away. The demo ships three live variants — top banner versus sidebar card, avatar preview versus text-only, and dismissible versus persistent — switchable from a segmented control.
MCP
Код
:root {
--brand: #5b5bf0;
--brand-d: #4646d6;
--brand-700: #3a3ab8;
--brand-50: #eef0ff;
--accent: #00b4a6;
--accent-soft: #d8f5f2;
--ink: #101322;
--ink-2: #3a4060;
--muted: #6c7393;
--bg: #f6f7fb;
--white: #ffffff;
--surface: #ffffff;
--line: rgba(16, 19, 34, 0.1);
--line-2: rgba(16, 19, 34, 0.16);
--ok: #2f9e6f;
--warn: #d98a2b;
--danger: #d4503e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-1: 0 1px 2px rgba(16, 19, 34, 0.08);
--sh-2: 0 8px 24px rgba(16, 19, 34, 0.08);
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.page {
max-width: 1040px;
margin: 0 auto;
padding: 40px 20px 64px;
}
/* ---------- Masthead + switcher ---------- */
.masthead {
margin-bottom: 28px;
}
.kicker {
margin: 0 0 8px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--brand);
}
.title {
margin: 0 0 8px;
font-size: clamp(24px, 4vw, 32px);
font-weight: 800;
letter-spacing: -0.02em;
}
.lede {
margin: 0 0 22px;
max-width: 62ch;
color: var(--ink-2);
font-size: 15px;
}
.switcher {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.seg {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 999px;
box-shadow: var(--sh-1);
}
.seg-label {
padding: 0 8px 0 10px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
}
.seg-btn {
appearance: none;
border: 0;
background: transparent;
padding: 7px 14px;
border-radius: 999px;
font: inherit;
font-size: 13px;
font-weight: 600;
color: var(--ink-2);
cursor: pointer;
transition: background 0.16s ease, color 0.16s ease;
}
.seg-btn:hover {
background: var(--brand-50);
color: var(--brand-700);
}
.seg-btn.is-active {
background: var(--brand);
color: #fff;
box-shadow: 0 2px 6px rgba(91, 91, 240, 0.32);
}
/* ---------- App shell ---------- */
.app-shell {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-2);
padding: 16px;
overflow: hidden;
}
.shell-grid {
display: grid;
grid-template-columns: 1fr 256px;
gap: 16px;
margin-top: 16px;
}
.shell-main {
display: grid;
gap: 16px;
min-width: 0;
}
.shell-card {
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 18px;
display: grid;
gap: 12px;
}
.shell-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
}
.shell-tile {
height: 84px;
border-radius: var(--r-md);
border: 1px solid var(--line);
background: linear-gradient(135deg, var(--brand-50), #fff);
}
.skeleton {
height: 12px;
border-radius: 6px;
background: linear-gradient(90deg, #e7e9f3, #f2f3fa, #e7e9f3);
background-size: 200% 100%;
animation: shimmer 1.6s ease-in-out infinite;
}
.sk-title {
height: 16px;
width: 46%;
}
.sk-line {
width: 100%;
}
.sk-line.short {
width: 64%;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* ---------- Sidebar rail ---------- */
.rail {
min-width: 0;
}
/* ---------- Shared nudge bits ---------- */
.nudge {
position: relative;
}
.nudge-avatar {
position: relative;
flex: none;
width: 44px;
height: 44px;
}
.avatar {
position: relative;
z-index: 1;
display: grid;
place-items: center;
width: 44px;
height: 44px;
border-radius: 50%;
background: linear-gradient(135deg, var(--brand), var(--accent));
color: #fff;
font-size: 15px;
font-weight: 700;
letter-spacing: 0.02em;
}
.avatar-ring {
position: absolute;
inset: -4px;
border-radius: 50%;
border: 2px dashed var(--brand);
opacity: 0.45;
}
.nudge-title {
margin: 0;
font-size: 14px;
font-weight: 600;
color: var(--ink);
}
.nudge-title strong[data-pct] {
color: var(--brand);
font-weight: 800;
}
.nudge-sub {
margin: 2px 0 0;
font-size: 13px;
color: var(--muted);
}
.nudge-sub strong {
color: var(--ink-2);
font-weight: 600;
}
.track {
height: 8px;
border-radius: 999px;
background: var(--brand-50);
overflow: hidden;
}
.track-fill {
display: block;
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, var(--brand), var(--accent));
transition: width 0.55s cubic-bezier(0.4, 0, 0.2, 1);
}
.nudge-steps {
margin: 6px 0 0;
font-size: 12px;
font-weight: 600;
color: var(--muted);
}
.nudge-steps span {
color: var(--brand-700);
}
/* ---------- Buttons ---------- */
.btn {
appearance: none;
border: 1px solid var(--line-2);
background: var(--surface);
color: var(--ink);
font: inherit;
font-size: 13px;
font-weight: 600;
padding: 9px 16px;
border-radius: var(--r-sm);
cursor: pointer;
transition: transform 0.1s ease, background 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
white-space: nowrap;
}
.btn:hover {
background: var(--bg);
}
.btn:active {
transform: translateY(1px);
}
.btn-primary {
background: var(--brand);
border-color: var(--brand);
color: #fff;
box-shadow: 0 2px 8px rgba(91, 91, 240, 0.3);
}
.btn-primary:hover {
background: var(--brand-d);
}
.btn-ghost {
background: transparent;
border-color: transparent;
color: var(--ink-2);
}
.btn-ghost:hover {
background: var(--line);
}
.btn-block {
width: 100%;
justify-content: center;
display: inline-flex;
align-items: center;
}
.icon-btn {
appearance: none;
border: 0;
background: transparent;
color: var(--muted);
width: 30px;
height: 30px;
display: grid;
place-items: center;
border-radius: var(--r-sm);
cursor: pointer;
transition: background 0.16s ease, color 0.16s ease;
}
.icon-btn:hover {
background: var(--line);
color: var(--ink);
}
:where(button):focus-visible {
outline: 3px solid rgba(91, 91, 240, 0.4);
outline-offset: 2px;
}
/* ---------- Banner variant ---------- */
.nudge--banner {
display: flex;
align-items: center;
gap: 16px;
padding: 14px 16px;
background: linear-gradient(180deg, #fff, var(--brand-50));
border: 1px solid var(--line);
border-radius: var(--r-md);
box-shadow: var(--sh-1);
}
.nudge--banner .nudge-body {
flex: 1 1 auto;
min-width: 0;
}
.nudge--banner .nudge-top {
display: flex;
align-items: baseline;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 8px;
}
.nudge--banner .nudge-steps {
margin-top: 5px;
}
.nudge--banner .nudge-actions {
display: flex;
align-items: center;
gap: 8px;
flex: none;
}
/* ---------- Sidebar variant ---------- */
.nudge--sidebar {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
box-shadow: var(--sh-1);
padding: 16px;
position: sticky;
top: 16px;
}
.card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 12px;
}
.nudge--sidebar .nudge-title {
margin-bottom: 10px;
}
.nudge--sidebar .track {
margin-bottom: 2px;
}
.checklist {
list-style: none;
margin: 14px 0;
padding: 0;
display: grid;
gap: 4px;
}
.check-item {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 8px;
border-radius: var(--r-sm);
font-size: 13px;
color: var(--ink-2);
transition: background 0.16s ease;
}
.check-item .tick {
flex: none;
width: 18px;
height: 18px;
border-radius: 50%;
border: 1.5px solid var(--line-2);
display: grid;
place-items: center;
color: transparent;
}
.check-item.is-done {
color: var(--muted);
}
.check-item.is-done .label {
text-decoration: line-through;
text-decoration-color: var(--line-2);
}
.check-item.is-done .tick {
background: var(--ok);
border-color: var(--ok);
color: #fff;
}
.check-item.is-next {
background: var(--brand-50);
color: var(--brand-700);
font-weight: 600;
}
.check-item.is-next .tick {
border-color: var(--brand);
}
.check-item .tick svg {
width: 11px;
height: 11px;
}
/* ---------- Success state ---------- */
.nudge-success {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
}
.nudge--banner .nudge-success {
padding: 2px 0;
}
.nudge-success--card {
flex-direction: column;
text-align: center;
gap: 10px;
padding: 6px 0 2px;
}
.success-mark {
flex: none;
width: 38px;
height: 38px;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--accent-soft);
color: var(--ok);
}
.nudge-success--card .success-mark {
width: 48px;
height: 48px;
animation: pop 0.4s cubic-bezier(0.2, 1.4, 0.5, 1) both;
}
@keyframes pop {
0% {
transform: scale(0.4);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.success-text {
margin: 0;
font-size: 14px;
color: var(--ink-2);
flex: 1;
}
.success-text strong {
color: var(--ink);
}
/* success visual chrome */
.nudge.is-complete.nudge--banner {
background: linear-gradient(180deg, #fff, var(--accent-soft));
border-color: rgba(0, 180, 166, 0.35);
}
.nudge.is-complete.nudge--sidebar {
border-color: rgba(0, 180, 166, 0.35);
}
/* ---------- Variant visibility (driven by app-shell data-attrs) ---------- */
.nudge--banner {
display: none;
}
.app-shell[data-layout="banner"] .nudge--banner {
display: flex;
}
.app-shell[data-layout="banner"] .rail .nudge--sidebar {
display: none;
}
.app-shell[data-layout="banner"] .rail {
display: none;
}
.app-shell[data-layout="sidebar"] .nudge--banner {
display: none;
}
.app-shell[data-layout="sidebar"] .rail {
display: block;
}
/* media: hide avatar when text-only */
.app-shell[data-media="text"] [data-media-slot] {
display: none;
}
/* dismiss: hide close buttons when persistent */
.app-shell[data-dismiss="no"] [data-dismiss-btn] {
display: none;
}
/* dismissed state */
.nudge.is-dismissed {
animation: collapse 0.32s ease forwards;
overflow: hidden;
pointer-events: none;
}
@keyframes collapse {
to {
opacity: 0;
transform: translateY(-6px) scale(0.98);
max-height: 0;
margin: 0;
padding-top: 0;
padding-bottom: 0;
}
}
/* ---------- Footer ---------- */
.footer-tools {
display: flex;
align-items: center;
gap: 14px;
margin-top: 18px;
flex-wrap: wrap;
}
.hint {
font-size: 12.5px;
color: var(--muted);
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translate(-50%, 14px);
background: var(--ink);
color: #fff;
font-size: 13px;
font-weight: 600;
padding: 11px 18px;
border-radius: 999px;
box-shadow: var(--sh-2);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
z-index: 50;
}
.toast.is-show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- Responsive ---------- */
@media (max-width: 820px) {
.shell-grid {
grid-template-columns: 1fr;
}
.app-shell[data-layout="sidebar"] .rail {
max-width: 320px;
}
.nudge--sidebar {
position: static;
}
}
@media (max-width: 520px) {
.page {
padding: 28px 14px 48px;
}
.switcher {
flex-direction: column;
align-items: stretch;
}
.seg {
border-radius: var(--r-md);
justify-content: flex-start;
flex-wrap: wrap;
}
.nudge--banner {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.nudge--banner .nudge-avatar {
margin: 0 auto;
}
.nudge--banner .nudge-actions {
justify-content: space-between;
}
.nudge--banner .btn-primary {
flex: 1;
}
.shell-row {
grid-template-columns: 1fr 1fr;
}
.app-shell[data-layout="sidebar"] .rail {
max-width: none;
}
}(function () {
"use strict";
/* ---------- Data: setup steps (realistic but fictional) ---------- */
var STEPS = [
{ id: "verify", label: "Verify your email", cta: "Verify email", done: true },
{ id: "name", label: "Add your full name", cta: "Add name", done: true },
{ id: "role", label: "Choose your role", cta: "Set role", done: true },
{ id: "photo", label: "Add a profile photo", cta: "Add photo", done: false },
{ id: "team", label: "Invite a teammate", cta: "Invite teammate", done: false }
];
var shell = document.querySelector(".app-shell");
var nudges = Array.prototype.slice.call(document.querySelectorAll(".nudge"));
var checklistEl = document.querySelector("[data-checklist]");
/* ---------- Toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer = null;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2200);
}
/* ---------- Derived state ---------- */
function doneCount() {
return STEPS.filter(function (s) {
return s.done;
}).length;
}
function nextStep() {
return STEPS.find(function (s) {
return !s.done;
}) || null;
}
function pct() {
return Math.round((doneCount() / STEPS.length) * 100);
}
/* ---------- Render the sidebar checklist ---------- */
function tickSvg() {
return (
'<svg viewBox="0 0 12 12" aria-hidden="true"><path d="M2.5 6.2l2.2 2.2L9.5 3.4" ' +
'stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>'
);
}
function renderChecklist() {
if (!checklistEl) return;
var next = nextStep();
checklistEl.innerHTML = STEPS.map(function (s) {
var cls = "check-item";
if (s.done) cls += " is-done";
else if (next && s.id === next.id) cls += " is-next";
return (
'<li class="' +
cls +
'"><span class="tick">' +
tickSvg() +
'</span><span class="label">' +
s.label +
"</span></li>"
);
}).join("");
}
/* ---------- Render percentages, bars, CTAs across both variants ---------- */
function render() {
var p = pct();
var done = doneCount();
var total = STEPS.length;
var next = nextStep();
var complete = p >= 100;
document.querySelectorAll("[data-pct]").forEach(function (el) {
el.textContent = p + "%";
});
document.querySelectorAll("[data-fill]").forEach(function (el) {
el.style.width = p + "%";
});
document.querySelectorAll("[data-progressbar]").forEach(function (el) {
el.setAttribute("aria-valuenow", String(p));
});
document.querySelectorAll("[data-done-count]").forEach(function (el) {
el.textContent = String(done);
});
document.querySelectorAll("[data-total-count]").forEach(function (el) {
el.textContent = String(total);
});
var nextLabel = next ? next.label : "All steps complete";
document.querySelectorAll("[data-next-label]").forEach(function (el) {
el.textContent = nextLabel;
});
document.querySelectorAll("[data-cta-label]").forEach(function (el) {
el.textContent = nextLabel;
});
document.querySelectorAll("[data-cta]").forEach(function (btn) {
// Banner CTA uses the short cta text; sidebar CTA has a span (data-cta-label)
if (!btn.querySelector("[data-cta-label]")) {
btn.textContent = next ? next.cta : "Done";
}
btn.disabled = complete;
});
renderChecklist();
// Swap success state per nudge
nudges.forEach(function (nudge) {
var success = nudge.querySelector("[data-success]");
var content = Array.prototype.slice
.call(nudge.children)
.filter(function (c) {
return !c.hasAttribute("data-success");
});
if (complete) {
nudge.classList.add("is-complete");
if (success) success.hidden = false;
content.forEach(function (c) {
c.style.display = "none";
});
} else {
nudge.classList.remove("is-complete");
if (success) success.hidden = true;
content.forEach(function (c) {
c.style.display = "";
});
}
});
}
/* ---------- Complete the next step ---------- */
function completeNext() {
var next = nextStep();
if (!next) return;
next.done = true;
render();
if (pct() >= 100) {
toast("Profile complete! All steps done.");
} else {
toast("Done: " + next.label);
}
}
/* ---------- Dismiss a nudge ---------- */
function dismiss(nudge) {
if (shell.getAttribute("data-dismiss") === "no") return;
nudge.classList.add("is-dismissed");
toast("Reminder dismissed");
}
/* ---------- Wire up CTA + dismiss buttons ---------- */
document.querySelectorAll("[data-cta]").forEach(function (btn) {
btn.addEventListener("click", completeNext);
});
document.querySelectorAll("[data-dismiss-btn]").forEach(function (btn) {
btn.addEventListener("click", function () {
var nudge = btn.closest(".nudge");
if (nudge) dismiss(nudge);
});
});
/* ---------- Esc dismisses the visible nudge (when dismissible) ---------- */
document.addEventListener("keydown", function (e) {
if (e.key !== "Escape") return;
if (shell.getAttribute("data-dismiss") === "no") return;
var layout = shell.getAttribute("data-layout");
var sel = layout === "banner" ? ".nudge--banner" : ".nudge--sidebar";
var nudge = document.querySelector(sel);
if (nudge && !nudge.classList.contains("is-dismissed")) dismiss(nudge);
});
/* ---------- Variant switcher (segmented controls) ---------- */
function bindSegGroup(attr, shellAttr) {
var btns = Array.prototype.slice.call(
document.querySelectorAll(".seg-btn[" + attr + "]")
);
btns.forEach(function (btn) {
btn.addEventListener("click", function () {
var group = btn.closest(".seg");
group.querySelectorAll(".seg-btn").forEach(function (b) {
b.classList.remove("is-active");
b.setAttribute("aria-checked", "false");
});
btn.classList.add("is-active");
btn.setAttribute("aria-checked", "true");
shell.setAttribute(shellAttr, btn.getAttribute(attr));
});
});
}
bindSegGroup("data-layout", "data-layout");
bindSegGroup("data-media", "data-media");
bindSegGroup("data-dismiss", "data-dismiss");
/* ---------- Reset ---------- */
document.getElementById("reset-btn").addEventListener("click", function () {
STEPS[3].done = false;
STEPS[4].done = false;
STEPS[0].done = true;
STEPS[1].done = true;
STEPS[2].done = true;
nudges.forEach(function (n) {
n.classList.remove("is-dismissed");
});
render();
toast("Demo reset to 60%");
});
/* ---------- Init ---------- */
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Onboarding — Profile/setup completion nudge bar</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="masthead">
<p class="kicker">Onboarding pattern</p>
<h1 class="title">Profile completion nudge bar</h1>
<p class="lede">
A slim, dismissible nudge that tracks setup progress and surfaces the
single next best action. Complete steps to advance the bar — at
100% it flips to a success state you can clear away.
</p>
<div class="switcher" role="group" aria-label="Demo variant controls">
<div class="seg" role="radiogroup" aria-label="Layout variant">
<span class="seg-label">Layout</span>
<button class="seg-btn is-active" type="button" role="radio" aria-checked="true" data-layout="banner">
Top banner
</button>
<button class="seg-btn" type="button" role="radio" aria-checked="false" data-layout="sidebar">
Sidebar card
</button>
</div>
<div class="seg" role="radiogroup" aria-label="Media variant">
<span class="seg-label">Media</span>
<button class="seg-btn is-active" type="button" role="radio" aria-checked="true" data-media="avatar">
Avatar preview
</button>
<button class="seg-btn" type="button" role="radio" aria-checked="false" data-media="text">
Text only
</button>
</div>
<div class="seg" role="radiogroup" aria-label="Dismiss variant">
<span class="seg-label">Behavior</span>
<button class="seg-btn is-active" type="button" role="radio" aria-checked="true" data-dismiss="yes">
Dismissible
</button>
<button class="seg-btn" type="button" role="radio" aria-checked="false" data-dismiss="no">
Persistent
</button>
</div>
</div>
</header>
<!-- Mock app shell so the nudge has a realistic home -->
<main class="app-shell" data-layout="banner" data-media="avatar" data-dismiss="yes">
<!-- ===== Banner variant (top of content) ===== -->
<aside class="nudge nudge--banner" id="nudge-banner" aria-labelledby="nudge-banner-title">
<div class="nudge-avatar" data-media-slot>
<span class="avatar" aria-hidden="true">AR</span>
<span class="avatar-ring" aria-hidden="true"></span>
</div>
<div class="nudge-body">
<div class="nudge-top">
<p class="nudge-title" id="nudge-banner-title">
Your profile is <strong data-pct>60%</strong> complete
</p>
<p class="nudge-sub" data-action-line>
Next: <strong data-next-label>Add a profile photo</strong>
</p>
</div>
<div class="track" role="progressbar" aria-label="Profile completion" aria-valuemin="0" aria-valuemax="100" data-progressbar>
<span class="track-fill" data-fill style="width:60%"></span>
</div>
<p class="nudge-steps" aria-live="polite" data-steps-line>
<span data-done-count>3</span> of <span data-total-count>5</span> steps done
</p>
</div>
<div class="nudge-actions">
<button class="btn btn-primary" type="button" data-cta>Add photo</button>
<button class="icon-btn" type="button" data-dismiss-btn aria-label="Dismiss this reminder">
<svg viewBox="0 0 16 16" width="15" height="15" aria-hidden="true">
<path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
</svg>
</button>
</div>
<!-- success swap -->
<div class="nudge-success" data-success hidden>
<span class="success-mark" aria-hidden="true">
<svg viewBox="0 0 20 20" width="18" height="18"><path d="M4 10.5l3.5 3.5L16 6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
<p class="success-text">
<strong>Profile complete!</strong> Nice work — you’re all set up.
</p>
<button class="btn btn-ghost" type="button" data-dismiss-btn>Dismiss</button>
</div>
</aside>
<div class="shell-grid">
<section class="shell-main" aria-label="Workspace preview">
<div class="shell-card">
<div class="skeleton sk-title"></div>
<div class="skeleton sk-line"></div>
<div class="skeleton sk-line short"></div>
</div>
<div class="shell-row">
<div class="shell-tile"></div>
<div class="shell-tile"></div>
<div class="shell-tile"></div>
</div>
<div class="shell-card">
<div class="skeleton sk-line"></div>
<div class="skeleton sk-line"></div>
<div class="skeleton sk-line short"></div>
</div>
</section>
<!-- ===== Sidebar variant ===== -->
<aside class="rail" aria-label="Setup sidebar">
<div class="nudge nudge--sidebar" id="nudge-sidebar" aria-labelledby="nudge-sidebar-title">
<div class="card-head">
<div class="nudge-avatar" data-media-slot>
<span class="avatar" aria-hidden="true">AR</span>
<span class="avatar-ring" aria-hidden="true"></span>
</div>
<button class="icon-btn" type="button" data-dismiss-btn aria-label="Dismiss this reminder">
<svg viewBox="0 0 16 16" width="15" height="15" aria-hidden="true">
<path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
</svg>
</button>
</div>
<p class="nudge-title" id="nudge-sidebar-title">
Your profile is <strong data-pct>60%</strong> complete
</p>
<div class="track" role="progressbar" aria-label="Profile completion" aria-valuemin="0" aria-valuemax="100" data-progressbar>
<span class="track-fill" data-fill style="width:60%"></span>
</div>
<p class="nudge-steps" aria-live="polite" data-steps-line>
<span data-done-count>3</span> of <span data-total-count>5</span> steps done
</p>
<ul class="checklist" data-checklist></ul>
<button class="btn btn-primary btn-block" type="button" data-cta>
<span data-cta-label>Add a profile photo</span>
</button>
<!-- success swap -->
<div class="nudge-success nudge-success--card" data-success hidden>
<span class="success-mark" aria-hidden="true">
<svg viewBox="0 0 20 20" width="20" height="20"><path d="M4 10.5l3.5 3.5L16 6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
<p class="success-text"><strong>All set!</strong> Your profile is complete.</p>
<button class="btn btn-ghost btn-block" type="button" data-dismiss-btn>Dismiss</button>
</div>
</div>
</aside>
</div>
</main>
<footer class="footer-tools">
<button class="btn btn-ghost" type="button" id="reset-btn">Reset demo</button>
<span class="hint">Press the action button to complete the next step.</span>
</footer>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Profile/setup completion nudge bar
A low-friction onboarding nudge that lives at the top of (or beside) the product. It states the completion percentage in plain language — Your profile is 60% complete — backs it with a gradient progress bar, and surfaces the one suggested next action rather than a wall of tasks. A primary button completes that step, the percentage and step counter advance with a smooth fill animation, and the next suggestion rolls forward automatically.
The same component is shown in three live variants, toggled from a segmented switcher. Layout swaps between a full-width top banner and a sticky sidebar card (which adds an inline checklist of all five steps with done / next states). Media turns the avatar preview on or off for a leaner text-only treatment. Behavior flips between dismissible — a close button plus Esc to collapse the bar — and persistent, where the nudge stays put until setup is finished.
Once all steps are complete the nudge transitions into a success state with a pop-in checkmark and a confirming message; in the dismissible variants it can then be cleared. A toast confirms each action, a reset control returns the demo to 60%, and the whole thing reflows to a single column down to 360px.
Illustrative UI only — fictional users, steps, and data.