Empty States — All-done / inbox-zero state
A task list that rewards finishing the job: tick the final item and the list animates away into a celebratory inbox-zero empty state with a drawn checkmark badge, a confetti micro-animation, and a friendly all-caught-up headline. A live segmented switcher toggles between a celebratory treatment with confetti and a next-action CTA, and a calm, minimal variant that simply settles. An Undo control (and the Escape key) restores the cleared items, an aria-live counter tracks how many tasks remain, and a toast confirms every action.
MCP
Kod
: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;
min-height: 100vh;
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;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 40px 20px;
}
.shell {
width: 100%;
max-width: 560px;
}
/* ---- Header ---- */
.app-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 18px;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
}
.brand-mark {
display: grid;
place-items: center;
width: 38px;
height: 38px;
border-radius: var(--r-md);
background: linear-gradient(140deg, var(--brand), var(--brand-700));
color: #fff;
box-shadow: var(--sh-1);
}
.brand-text {
display: flex;
flex-direction: column;
line-height: 1.25;
}
.brand-text strong {
font-size: 16px;
font-weight: 700;
}
.brand-text span {
font-size: 12.5px;
color: var(--muted);
}
/* ---- Segmented variant switch ---- */
.variant-switch {
display: inline-flex;
padding: 4px;
background: var(--white);
border: 1px solid var(--line);
border-radius: 999px;
box-shadow: var(--sh-1);
}
.seg {
appearance: none;
border: 0;
background: transparent;
font: inherit;
font-size: 13px;
font-weight: 600;
color: var(--muted);
padding: 7px 14px;
border-radius: 999px;
cursor: pointer;
transition: background 0.18s, color 0.18s;
}
.seg:hover {
color: var(--ink-2);
}
.seg.is-on {
background: var(--brand-50);
color: var(--brand-d);
}
.seg:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
}
/* ---- Card ---- */
.card {
position: relative;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-2);
padding: 22px 22px 12px;
overflow: hidden;
}
.card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 6px;
}
h1 {
margin: 0;
font-size: 19px;
font-weight: 700;
letter-spacing: -0.01em;
}
.sub {
margin: 2px 0 0;
font-size: 13px;
color: var(--muted);
}
.count-pill {
flex: none;
font-size: 12px;
font-weight: 600;
color: var(--brand-d);
background: var(--brand-50);
border-radius: 999px;
padding: 5px 11px;
transition: background 0.2s, color 0.2s;
}
.count-pill.is-clear {
color: var(--ok);
background: rgba(47, 158, 111, 0.12);
}
/* ---- Task list ---- */
.task-list {
list-style: none;
margin: 12px 0 6px;
padding: 0;
}
.task {
display: flex;
align-items: center;
gap: 13px;
padding: 12px 10px;
border-radius: var(--r-md);
transition: background 0.16s, opacity 0.3s, transform 0.3s;
}
.task:hover {
background: var(--bg);
}
.task + .task {
border-top: 1px solid var(--line);
}
.check {
appearance: none;
flex: none;
width: 22px;
height: 22px;
border: 2px solid var(--line-2);
border-radius: 7px;
cursor: pointer;
display: grid;
place-items: center;
background: var(--white);
transition: background 0.16s, border-color 0.16s, transform 0.12s;
}
.check::after {
content: "";
width: 11px;
height: 11px;
background: #fff;
clip-path: polygon(14% 47%, 0 60%, 40% 100%, 100% 22%, 86% 9%, 38% 70%);
transform: scale(0);
transition: transform 0.16s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.check:hover {
border-color: var(--brand);
}
.check:active {
transform: scale(0.92);
}
.check:checked {
background: var(--brand);
border-color: var(--brand);
}
.check:checked::after {
transform: scale(1);
}
.check:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
}
.task-main {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
}
.task-title {
font-size: 14.5px;
font-weight: 600;
color: var(--ink);
cursor: pointer;
transition: color 0.2s;
}
.task-meta {
font-size: 12px;
color: var(--muted);
margin-top: 1px;
}
.tag {
flex: none;
font-size: 11px;
font-weight: 600;
padding: 3px 9px;
border-radius: 999px;
letter-spacing: 0.01em;
}
.tag.focus {
color: var(--brand-d);
background: var(--brand-50);
}
.tag.review {
color: var(--warn);
background: rgba(217, 138, 43, 0.13);
}
.tag.urgent {
color: var(--danger);
background: rgba(212, 80, 62, 0.12);
}
.tag.errand {
color: var(--accent);
background: var(--accent-soft);
}
.task.done .task-title {
color: var(--muted);
text-decoration: line-through;
}
.task.leaving {
opacity: 0;
transform: translateX(14px);
}
/* ---- Cleared / empty state ---- */
.cleared {
position: relative;
text-align: center;
padding: 30px 18px 38px;
}
.cleared h2 {
margin: 16px 0 6px;
font-size: 21px;
font-weight: 800;
letter-spacing: -0.015em;
color: var(--ink);
}
.cleared p {
margin: 0 auto;
max-width: 320px;
font-size: 14px;
color: var(--muted);
}
.badge-burst {
display: inline-block;
animation: pop 0.55s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
.tick {
stroke-dasharray: 60;
stroke-dashoffset: 60;
animation: draw 0.5s 0.18s ease forwards;
}
.cleared-actions {
display: flex;
align-items: center;
justify-content: center;
gap: 18px;
margin-top: 20px;
flex-wrap: wrap;
}
.next-link {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 600;
color: var(--brand-d);
text-decoration: none;
padding: 9px 14px;
border-radius: var(--r-md);
background: var(--brand-50);
transition: background 0.16s, transform 0.12s;
}
.next-link:hover {
background: #e3e6ff;
}
.next-link:active {
transform: translateY(1px);
}
.next-link svg {
transition: transform 0.16s;
}
.next-link:hover svg {
transform: translateX(2px);
}
.next-link:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
}
.undo-btn {
display: inline-flex;
align-items: center;
gap: 6px;
font: inherit;
font-size: 14px;
font-weight: 600;
color: var(--ink-2);
background: transparent;
border: 1px solid var(--line-2);
border-radius: var(--r-md);
padding: 9px 14px;
cursor: pointer;
transition: background 0.16s, border-color 0.16s;
}
.undo-btn:hover {
background: var(--bg);
border-color: var(--brand);
color: var(--brand-d);
}
.undo-btn:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
}
/* Calm variant overrides */
.card.calm .badge-burst {
animation: fade-up 0.45s ease both;
}
.card.calm .cleared h2 {
font-weight: 700;
font-size: 19px;
}
.card.calm .confetti {
display: none;
}
.card.calm .next-link {
background: transparent;
border: 1px solid var(--line-2);
}
.card.calm .next-link:hover {
background: var(--bg);
}
/* ---- Confetti ---- */
.confetti {
position: absolute;
inset: 0;
pointer-events: none;
overflow: hidden;
}
.confetti i {
position: absolute;
top: -12px;
width: 8px;
height: 13px;
border-radius: 2px;
opacity: 0;
animation: fall linear forwards;
}
/* ---- Hint footer ---- */
.hint {
margin: 16px 4px 0;
font-size: 12.5px;
color: var(--muted);
text-align: center;
}
.text-btn {
font: inherit;
font-size: 12.5px;
font-weight: 600;
color: var(--brand-d);
background: none;
border: 0;
padding: 0;
cursor: pointer;
text-decoration: underline;
text-underline-offset: 2px;
}
.text-btn:hover {
color: var(--brand-700);
}
.text-btn:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
border-radius: 4px;
}
/* ---- Toast ---- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 16px);
background: var(--ink);
color: #fff;
font-size: 13.5px;
font-weight: 500;
padding: 11px 18px;
border-radius: 999px;
box-shadow: var(--sh-2);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s, transform 0.22s;
z-index: 40;
max-width: calc(100% - 32px);
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---- Animations ---- */
@keyframes draw {
to {
stroke-dashoffset: 0;
}
}
@keyframes pop {
0% {
transform: scale(0.4);
opacity: 0;
}
70% {
transform: scale(1.08);
}
100% {
transform: scale(1);
opacity: 1;
}
}
@keyframes fade-up {
from {
transform: translateY(10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes fall {
0% {
transform: translateY(0) rotate(0);
opacity: 1;
}
100% {
transform: translateY(360px) rotate(540deg);
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::after {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}
/* ---- Responsive ---- */
@media (max-width: 520px) {
body {
padding: 22px 14px;
}
.app-head {
flex-direction: column;
align-items: stretch;
}
.variant-switch {
align-self: stretch;
justify-content: center;
}
.seg {
flex: 1;
text-align: center;
}
.card {
padding: 18px 16px 10px;
}
.cleared-actions {
flex-direction: column;
gap: 12px;
}
.next-link,
.undo-btn {
width: 100%;
justify-content: center;
}
}(function () {
"use strict";
var SEED = [
{ id: "t1", title: "Reply to Priya about the Q3 rollout", meta: "Inbox · due 11:00", tag: "urgent", tagLabel: "Urgent" },
{ id: "t2", title: "Review the Driftboard onboarding copy", meta: "Design · 20 min", tag: "review", tagLabel: "Review" },
{ id: "t3", title: "Draft the sprint demo outline", meta: "Today · 45 min", tag: "focus", tagLabel: "Focus" },
{ id: "t4", title: "Pick up dry cleaning before 6pm", meta: "Personal", tag: "errand", tagLabel: "Errand" }
];
var COPY = {
celebratory: {
title: "You’re all caught up",
sub: "Inbox zero. Nothing left on your plate for today.",
link: "Plan tomorrow’s focus"
},
calm: {
title: "All clear",
sub: "Nothing else to do right now. Take the win.",
link: "Review tomorrow"
}
};
var CONFETTI_COLORS = ["#5b5bf0", "#00b4a6", "#d98a2b", "#d4503e", "#3a3ab8"];
var listEl = document.getElementById("task-list");
var clearedEl = document.getElementById("cleared");
var cardEl = document.querySelector(".card");
var countPill = document.getElementById("count-pill");
var confettiEl = document.getElementById("confetti");
var toastEl = document.getElementById("toast");
var clearedTitle = document.getElementById("cleared-title");
var clearedSub = document.getElementById("cleared-sub");
var nextLink = document.getElementById("next-link");
var tasks = [];
var variant = "celebratory";
var toastTimer = null;
var leaveTimer = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2400);
}
function remaining() {
return tasks.filter(function (t) {
return !t.done;
}).length;
}
function render() {
listEl.innerHTML = "";
tasks.forEach(function (t) {
var li = document.createElement("li");
li.className = "task" + (t.done ? " done" : "");
li.dataset.id = t.id;
var cb = document.createElement("input");
cb.type = "checkbox";
cb.className = "check";
cb.checked = t.done;
cb.id = "cb-" + t.id;
cb.setAttribute("aria-label", "Mark complete: " + t.title);
cb.addEventListener("change", function () {
toggle(t.id, cb.checked);
});
var main = document.createElement("div");
main.className = "task-main";
var title = document.createElement("label");
title.className = "task-title";
title.setAttribute("for", "cb-" + t.id);
title.textContent = t.title;
var meta = document.createElement("span");
meta.className = "task-meta";
meta.textContent = t.meta;
main.appendChild(title);
main.appendChild(meta);
var tag = document.createElement("span");
tag.className = "tag " + t.tag;
tag.textContent = t.tagLabel;
li.appendChild(cb);
li.appendChild(main);
li.appendChild(tag);
listEl.appendChild(li);
});
updateCount();
}
function updateCount() {
var left = remaining();
if (left === 0) {
countPill.textContent = "All done";
countPill.classList.add("is-clear");
} else {
countPill.textContent = left + (left === 1 ? " left" : " left");
countPill.classList.remove("is-clear");
}
}
function toggle(id, done) {
var task = tasks.find(function (t) {
return t.id === id;
});
if (!task) return;
task.done = done;
var li = listEl.querySelector('[data-id="' + id + '"]');
if (li) li.classList.toggle("done", done);
updateCount();
if (remaining() === 0) {
showCleared();
}
}
function applyCopy() {
var c = COPY[variant];
clearedTitle.textContent = c.title;
clearedSub.textContent = c.sub;
nextLink.firstChild.nodeValue = c.link + " ";
}
function showCleared() {
if (leaveTimer) clearTimeout(leaveTimer);
var items = Array.prototype.slice.call(listEl.children);
items.forEach(function (li, i) {
setTimeout(function () {
li.classList.add("leaving");
}, i * 60);
});
leaveTimer = setTimeout(function () {
listEl.hidden = true;
applyCopy();
clearedEl.hidden = false;
if (variant === "celebratory") burst();
}, 240 + items.length * 60);
}
function hideCleared() {
clearedEl.hidden = true;
listEl.hidden = false;
confettiEl.innerHTML = "";
}
function burst() {
confettiEl.innerHTML = "";
if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
var n = 46;
for (var i = 0; i < n; i++) {
var p = document.createElement("i");
p.style.left = Math.random() * 100 + "%";
p.style.background = CONFETTI_COLORS[i % CONFETTI_COLORS.length];
p.style.animationDuration = 1 + Math.random() * 0.9 + "s";
p.style.animationDelay = Math.random() * 0.25 + "s";
var w = 6 + Math.random() * 5;
p.style.width = w + "px";
p.style.height = w + Math.random() * 6 + "px";
confettiEl.appendChild(p);
}
setTimeout(function () {
confettiEl.innerHTML = "";
}, 2600);
}
function undo() {
hideCleared();
// restore the most-recently completed item; if all done, restore the last one
var restored = null;
for (var i = tasks.length - 1; i >= 0; i--) {
if (tasks[i].done) {
tasks[i].done = false;
restored = tasks[i];
break;
}
}
render();
if (restored) {
toast("Restored “" + restored.title.slice(0, 32) + (restored.title.length > 32 ? "…" : "") + "”");
}
}
function reset() {
hideCleared();
tasks = SEED.map(function (t) {
return Object.assign({}, t, { done: false });
});
render();
toast("Demo reset — 4 tasks restored");
}
function setVariant(v) {
variant = v;
cardEl.classList.toggle("calm", v === "calm");
document.querySelectorAll(".seg").forEach(function (b) {
var on = b.dataset.variant === v;
b.classList.toggle("is-on", on);
b.setAttribute("aria-checked", on ? "true" : "false");
});
if (!clearedEl.hidden) {
applyCopy();
if (v === "celebratory") burst();
else confettiEl.innerHTML = "";
}
}
// ---- Wire up controls ----
document.querySelectorAll(".seg").forEach(function (b) {
b.addEventListener("click", function () {
setVariant(b.dataset.variant);
});
});
// arrow-key navigation on segmented control
var segWrap = document.querySelector(".variant-switch");
segWrap.addEventListener("keydown", function (e) {
if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
e.preventDefault();
var segs = Array.prototype.slice.call(segWrap.querySelectorAll(".seg"));
var idx = segs.findIndex(function (s) {
return s.classList.contains("is-on");
});
var next = e.key === "ArrowRight" ? (idx + 1) % segs.length : (idx - 1 + segs.length) % segs.length;
segs[next].focus();
setVariant(segs[next].dataset.variant);
});
document.getElementById("undo-btn").addEventListener("click", undo);
document.getElementById("reset-btn").addEventListener("click", reset);
nextLink.addEventListener("click", function (e) {
e.preventDefault();
toast("Opening tomorrow’s plan…");
});
// Esc on the cleared state restores items (overlay-style affordance)
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && !clearedEl.hidden) {
undo();
}
});
reset();
// initial reset toasts; clear it so the first paint is quiet
toastEl.classList.remove("show");
toastEl.textContent = "";
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Empty States — All-done / inbox-zero</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>
<main class="shell">
<header class="app-head">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 6h16M4 12h16M4 18h10" />
</svg>
</span>
<div class="brand-text">
<strong>Driftboard</strong>
<span>Today · Tuesday</span>
</div>
</div>
<div class="variant-switch" role="radiogroup" aria-label="Empty-state style">
<button class="seg is-on" role="radio" aria-checked="true" data-variant="celebratory">Celebratory</button>
<button class="seg" role="radio" aria-checked="false" data-variant="calm">Calm</button>
</div>
</header>
<section class="card" aria-labelledby="list-title">
<div class="card-head">
<div>
<h1 id="list-title">My tasks</h1>
<p class="sub">Check things off as you finish them.</p>
</div>
<span class="count-pill" aria-live="polite" id="count-pill">4 left</span>
</div>
<ul class="task-list" id="task-list" aria-label="Task list"></ul>
<!-- All-done empty state -->
<div class="cleared" id="cleared" hidden>
<div class="confetti" id="confetti" aria-hidden="true"></div>
<div class="badge-burst" aria-hidden="true">
<svg viewBox="0 0 96 96" width="96" height="96" fill="none">
<circle cx="48" cy="48" r="42" fill="var(--accent-soft)"/>
<circle cx="48" cy="48" r="42" stroke="var(--accent)" stroke-width="3"/>
<path class="tick" d="M31 49l11 11 23-25" stroke="var(--accent)" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h2 id="cleared-title">You’re all caught up</h2>
<p id="cleared-sub">Inbox zero. Nothing left on your plate for today.</p>
<div class="cleared-actions">
<a href="#" class="next-link" id="next-link">
Plan tomorrow’s focus
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
</a>
<button class="undo-btn" id="undo-btn" type="button">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 14L4 9l5-5"/><path d="M4 9h11a5 5 0 0 1 5 5v1a5 5 0 0 1-5 5H8"/></svg>
Undo
</button>
</div>
</div>
</section>
<p class="hint">Tip: tick every task to reach the all-done state. <button class="text-btn" id="reset-btn" type="button">Reset demo</button></p>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>All-done / inbox-zero state
A compact task card sits inside a Driftboard-style product shell, listing four realistic, clearly fictional to-dos with colour-coded tags (Urgent, Review, Focus, Errand) and a live “left” counter in the header. Checking a box strikes the item through; checking off the last remaining task triggers the payoff — the rows stagger out and the card swaps to a centred inbox-zero empty state featuring an SVG checkmark badge that draws itself in, a bold “You’re all caught up” headline, and a subtle next-action link.
The pattern ships with two variants exposed through a segmented switcher in the header. The celebratory treatment pops the badge, fires a short CSS/JS confetti burst, and surfaces a “Plan tomorrow’s focus” CTA; the calm treatment skips the confetti, settles the badge with a gentle fade, and keeps the copy understated. Switching variants while the empty state is showing re-renders it live, so you can compare both side by side without resetting.
Every interaction is reversible and accessible. An Undo button — or the Escape key — restores the most recently cleared items and returns you to the list, the header counter is wired with aria-live for screen readers, the segmented control is arrow-key navigable, all controls have focus-visible rings, and a small toast confirms undo and reset actions. The layout collapses to a single column down to 360px, and a reduced-motion media query disables the confetti and draw animations.
Illustrative UI only — fictional product, names, and tasks.