SaaS — App Dashboard / Home
A polished in-app SaaS dashboard home built with vanilla HTML, CSS, and JavaScript. It pairs a time-aware greeting and quick-actions row with four KPI cards that carry deltas and inline SVG sparklines, a primary revenue trend chart with hover tooltips, a dismissable onboarding nudge, a checkable tasks widget, and a recent-activity feed. Switching the date range recomputes every metric and redraws the charts live, and a working light and dark theme toggle keeps the whole shell on-brand.
MCP
Code
:root {
--bg: #f7f8fb;
--surface: #ffffff;
--surface-2: #f2f3f8;
--ink: #0f1222;
--muted: #646b85;
--brand: #6366f1;
--brand-d: #4f46e5;
--ok: #16a34a;
--warn: #d97706;
--danger: #dc2626;
--line: rgba(15, 18, 34, .1);
--shadow: 0 1px 2px rgba(15, 18, 34, .04), 0 8px 24px rgba(15, 18, 34, .06);
--radius: 14px;
}
[data-theme="dark"] {
--bg: #0c0e17;
--surface: #14172300;
--surface: #141723;
--surface-2: #1b1f30;
--ink: #eef0f7;
--muted: #99a0bb;
--brand: #818cf8;
--brand-d: #6366f1;
--line: rgba(255, 255, 255, .1);
--shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 10px 30px rgba(0, 0, 0, .4);
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--ink);
font-family: "Inter", system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition: background .25s ease, color .25s ease;
}
button { font-family: inherit; }
:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
border-radius: 6px;
}
/* ---------- Layout ---------- */
.app {
display: grid;
grid-template-columns: 248px 1fr;
min-height: 100vh;
}
/* ---------- Sidebar ---------- */
.sidebar {
background: var(--surface);
border-right: 1px solid var(--line);
padding: 20px 16px;
display: flex;
flex-direction: column;
gap: 8px;
position: sticky;
top: 0;
height: 100vh;
}
.brand { display: flex; align-items: center; gap: 10px; padding: 6px 8px 18px; }
.brand-mark {
display: grid; place-items: center;
width: 32px; height: 32px; border-radius: 9px;
background: linear-gradient(135deg, var(--brand), var(--brand-d));
color: #fff;
}
.brand-name { font-weight: 700; font-size: 1.05rem; letter-spacing: -.01em; }
.nav { display: flex; flex-direction: column; gap: 2px; }
.nav-link {
display: flex; align-items: center; gap: 11px;
padding: 9px 11px; border-radius: 10px;
text-decoration: none; color: var(--muted);
font-size: .92rem; font-weight: 500;
transition: background .15s, color .15s;
}
.nav-link .ni { width: 18px; text-align: center; font-size: .9rem; }
.nav-link:hover { background: var(--surface-2); color: var(--ink); }
.nav-link.active { background: color-mix(in srgb, var(--brand) 14%, transparent); color: var(--brand-d); }
[data-theme="dark"] .nav-link.active { color: var(--ink); }
.plan-card {
margin-top: auto;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: 12px;
padding: 14px;
}
.plan-name { margin: 0 0 2px; font-weight: 600; font-size: .9rem; }
.plan-meta { margin: 0 0 9px; font-size: .78rem; color: var(--muted); }
.plan-bar { height: 6px; border-radius: 99px; background: var(--line); overflow: hidden; margin-bottom: 11px; }
.plan-bar span { display: block; height: 100%; border-radius: 99px; background: linear-gradient(90deg, var(--brand), var(--brand-d)); }
/* ---------- Main ---------- */
.main { display: flex; flex-direction: column; min-width: 0; }
.topbar {
display: flex; align-items: center; gap: 16px;
padding: 14px 28px;
border-bottom: 1px solid var(--line);
background: color-mix(in srgb, var(--surface) 80%, transparent);
backdrop-filter: blur(8px);
position: sticky; top: 0; z-index: 20;
}
.search {
display: flex; align-items: center; gap: 8px;
flex: 1; max-width: 460px;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: 10px;
padding: 8px 12px;
}
.search input {
border: none; background: transparent; outline: none;
width: 100%; color: var(--ink); font-size: .9rem;
}
.search input::placeholder { color: var(--muted); }
.topbar-actions { display: flex; align-items: center; gap: 8px; margin-left: auto; }
.icon-btn {
display: grid; place-items: center;
width: 38px; height: 38px;
border: 1px solid var(--line); border-radius: 10px;
background: var(--surface); color: var(--ink);
cursor: pointer; font-size: .95rem;
transition: background .15s, transform .1s;
}
.icon-btn:hover { background: var(--surface-2); }
.icon-btn:active { transform: scale(.95); }
.theme-ico { width: 16px; height: 16px; border-radius: 50%; background: var(--warn); box-shadow: inset -5px -4px 0 0 var(--surface); }
[data-theme="dark"] .theme-ico { background: var(--brand); box-shadow: none; }
.avatar {
display: grid; place-items: center;
width: 38px; height: 38px; border-radius: 50%;
background: linear-gradient(135deg, #f59e0b, #ef4444);
color: #fff; font-size: .8rem; font-weight: 600;
}
.content {
padding: 24px 28px 48px;
max-width: 1240px;
width: 100%;
margin: 0 auto;
}
/* ---------- Greeting ---------- */
.greet {
display: flex; align-items: flex-end; justify-content: space-between;
gap: 16px; flex-wrap: wrap; margin-bottom: 20px;
}
.greet h1 { margin: 0; font-size: 1.6rem; letter-spacing: -.02em; }
.greet .sub { margin: 4px 0 0; color: var(--muted); font-size: .95rem; }
.quick-actions { display: flex; gap: 8px; flex-wrap: wrap; }
/* ---------- Buttons ---------- */
.btn {
border: 1px solid var(--line); border-radius: 10px;
padding: 9px 15px; font-size: .9rem; font-weight: 600;
cursor: pointer; color: var(--ink); background: var(--surface);
transition: background .15s, transform .1s, border-color .15s;
}
.btn:hover { background: var(--surface-2); }
.btn:active { transform: translateY(1px); }
.btn-primary { background: var(--brand); border-color: var(--brand); color: #fff; }
.btn-primary:hover { background: var(--brand-d); border-color: var(--brand-d); }
.btn-ghost { background: transparent; }
.btn-sm { padding: 6px 11px; font-size: .82rem; }
/* ---------- Nudge ---------- */
.nudge {
display: flex; align-items: center; gap: 16px;
background: linear-gradient(120deg, color-mix(in srgb, var(--brand) 9%, var(--surface)), var(--surface));
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 16px 18px;
margin-bottom: 24px;
box-shadow: var(--shadow);
}
.nudge.dismissed { display: none; }
.nudge-ring { position: relative; flex: none; width: 44px; height: 44px; }
.nudge-ring svg { transform: rotate(-90deg); }
.ring-bg { fill: none; stroke: var(--line); stroke-width: 4; }
.ring-fg { fill: none; stroke: var(--brand); stroke-width: 4; stroke-linecap: round; transition: stroke-dashoffset .6s ease; }
.ring-num { position: absolute; inset: 0; display: grid; place-items: center; font-size: .68rem; font-weight: 700; }
.nudge-body { flex: 1; min-width: 0; }
.nudge-title { margin: 0; font-weight: 600; font-size: .95rem; }
.nudge-text { margin: 2px 0 0; color: var(--muted); font-size: .85rem; }
.nudge-close { width: 32px; height: 32px; font-size: .8rem; }
/* ---------- Toolbar ---------- */
.toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.section-h { margin: 0; font-size: 1.05rem; font-weight: 600; }
.range { display: inline-flex; background: var(--surface-2); border: 1px solid var(--line); border-radius: 10px; padding: 3px; gap: 2px; }
.range-btn {
border: none; background: transparent; cursor: pointer;
padding: 6px 13px; border-radius: 7px; font-size: .84rem; font-weight: 600;
color: var(--muted); transition: background .15s, color .15s;
}
.range-btn:hover { color: var(--ink); }
.range-btn.active { background: var(--surface); color: var(--ink); box-shadow: var(--shadow); }
[data-theme="dark"] .range-btn.active { background: var(--brand); color: #fff; }
/* ---------- Cards ---------- */
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 18px;
}
.card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
.card-title { margin: 0; font-size: 1rem; font-weight: 600; }
.card-sub { margin: 2px 0 0; font-size: .82rem; color: var(--muted); }
/* ---------- KPIs ---------- */
.kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 16px; }
.kpi { display: flex; flex-direction: column; gap: 8px; }
.kpi-top { display: flex; align-items: center; justify-content: space-between; }
.kpi-label { font-size: .82rem; color: var(--muted); font-weight: 500; }
.kpi-value { margin: 0; font-size: 1.65rem; font-weight: 700; letter-spacing: -.02em; }
.delta { font-size: .76rem; font-weight: 700; padding: 2px 7px; border-radius: 99px; }
.delta.up { color: var(--ok); background: color-mix(in srgb, var(--ok) 13%, transparent); }
.delta.down { color: var(--danger); background: color-mix(in srgb, var(--danger) 13%, transparent); }
.spark { width: 100%; height: 32px; color: var(--brand); margin-top: 2px; }
.kpi[data-kpi="conv"] .spark, .kpi[data-kpi="churn"] .spark { color: var(--warn); }
/* ---------- Grid ---------- */
.grid {
display: grid;
grid-template-columns: 2fr 1fr;
grid-template-areas: "chart tasks" "chart activity";
gap: 16px;
}
.chart-card { grid-area: chart; }
.widget:nth-of-type(2) { grid-area: tasks; }
.activity { grid-area: activity; }
/* ---------- Chart ---------- */
.legend { display: flex; gap: 12px; align-items: center; }
.lg { font-size: .76rem; color: var(--muted); display: inline-flex; align-items: center; gap: 6px; }
.lg::before { content: ""; width: 11px; height: 3px; border-radius: 2px; }
.lg-a::before { background: var(--brand); }
.lg-b::before { background: var(--muted); }
.chart-wrap { position: relative; }
#chart { width: 100%; height: 240px; display: block; overflow: visible; }
#gridlines line { stroke: var(--line); stroke-width: 1; }
.tip {
position: absolute; pointer-events: none;
background: var(--ink); color: var(--bg);
padding: 6px 9px; border-radius: 8px;
font-size: .76rem; font-weight: 600; white-space: nowrap;
transform: translate(-50%, -130%);
box-shadow: var(--shadow); z-index: 5;
}
/* ---------- Tasks ---------- */
.task-progress { margin: 0 0 10px; font-size: .84rem; color: var(--muted); }
.task-progress strong { color: var(--ink); }
.tasks { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.task {
display: flex; align-items: center; gap: 10px;
padding: 8px 6px; border-radius: 9px;
cursor: pointer; transition: background .15s;
}
.task:hover { background: var(--surface-2); }
.task .check {
flex: none; width: 19px; height: 19px; border-radius: 6px;
border: 1.5px solid var(--line); display: grid; place-items: center;
font-size: .7rem; color: #fff; transition: background .15s, border-color .15s;
}
.task.done .check { background: var(--ok); border-color: var(--ok); }
.task-label { font-size: .89rem; flex: 1; }
.task.done .task-label { color: var(--muted); text-decoration: line-through; }
.task .due { font-size: .72rem; color: var(--muted); font-weight: 600; }
/* ---------- Menu ---------- */
.menu-wrap { position: relative; }
.menu {
position: absolute; right: 0; top: 110%;
background: var(--surface); border: 1px solid var(--line);
border-radius: 11px; box-shadow: var(--shadow);
padding: 5px; min-width: 168px; z-index: 30;
display: flex; flex-direction: column;
}
.menu button {
text-align: left; border: none; background: transparent;
padding: 8px 10px; border-radius: 7px; cursor: pointer;
font-size: .85rem; color: var(--ink);
}
.menu button:hover { background: var(--surface-2); }
/* ---------- Activity feed ---------- */
.feed { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.feed li { display: flex; gap: 11px; padding: 10px 0; border-bottom: 1px solid var(--line); }
.feed li:last-child { border-bottom: none; }
.feed .fi {
flex: none; width: 30px; height: 30px; border-radius: 8px;
display: grid; place-items: center; font-size: .82rem;
background: var(--surface-2);
}
.feed .ft { font-size: .85rem; }
.feed .ft b { font-weight: 600; }
.feed .fm { font-size: .74rem; color: var(--muted); margin-top: 1px; }
/* ---------- Toast ---------- */
.toast {
position: fixed; left: 50%; bottom: 26px; transform: translate(-50%, 16px);
background: var(--ink); color: var(--bg);
padding: 11px 18px; border-radius: 11px; font-size: .88rem; font-weight: 600;
box-shadow: var(--shadow); opacity: 0; pointer-events: none;
transition: opacity .2s, transform .2s; z-index: 100;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- Responsive ---------- */
@media (max-width: 980px) {
.grid { grid-template-columns: 1fr; grid-template-areas: "chart" "tasks" "activity"; }
.kpis { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 820px) {
.app { grid-template-columns: 1fr; }
.sidebar {
position: static; height: auto; flex-direction: row;
align-items: center; flex-wrap: wrap; gap: 10px;
}
.nav { flex-direction: row; flex-wrap: wrap; flex: 1; }
.plan-card { display: none; }
}
@media (max-width: 560px) {
.content { padding: 18px 16px 40px; }
.topbar { padding: 12px 16px; }
.search { display: none; }
.kpis { grid-template-columns: 1fr; }
.greet h1 { font-size: 1.35rem; }
.nudge { flex-wrap: wrap; }
.nudge-close { position: absolute; }
}(function () {
"use strict";
/* ---------- Toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2400);
}
document.querySelectorAll("[data-toast]").forEach(function (btn) {
btn.addEventListener("click", function () { toast(btn.getAttribute("data-toast")); });
});
/* ---------- Greeting by time ---------- */
(function () {
var h = new Date().getHours();
var word = h < 12 ? "Good morning" : h < 18 ? "Good afternoon" : "Good evening";
document.getElementById("greeting").textContent = word + ", Ari";
})();
/* ---------- Theme toggle ---------- */
var root = document.documentElement;
document.getElementById("themeToggle").addEventListener("click", function () {
var dark = root.getAttribute("data-theme") === "dark";
root.setAttribute("data-theme", dark ? "light" : "dark");
toast(dark ? "Light theme" : "Dark theme");
render(); // redraw chart so gradient/colors stay crisp
});
/* ---------- Deterministic pseudo-random ---------- */
function seeded(seed) {
return function () {
seed = (seed * 9301 + 49297) % 233280;
return seed / 233280;
};
}
/* ---------- KPI definitions ---------- */
var KPIS = {
mrr: { base: 48200, vol: 0.11, fmt: money, trend: 1 },
users: { base: 3140, vol: 0.09, fmt: thousand, trend: 1 },
conv: { base: 3.8, vol: 0.14, fmt: pct1, trend: 1 },
churn: { base: 2.1, vol: 0.18, fmt: pct1, trend: -1 }
};
function money(v) { return "$" + Math.round(v).toLocaleString("en-US"); }
function thousand(v) { return Math.round(v).toLocaleString("en-US"); }
function pct1(v) { return v.toFixed(1) + "%"; }
/* Build a series of `n` points for a kpi + range seed */
function series(key, n, rangeSeed) {
var cfg = KPIS[key];
var rnd = seeded(rangeSeed + key.charCodeAt(0) * 17);
var pts = [];
var v = cfg.base * (1 - cfg.trend * 0.12); // start lower so trend reads up
for (var i = 0; i < n; i++) {
var drift = cfg.trend * (cfg.base * 0.24 / n);
var noise = (rnd() - 0.5) * cfg.base * cfg.vol;
v = Math.max(cfg.base * 0.4, v + drift + noise);
pts.push(v);
}
return pts;
}
function deltaPct(pts) {
var first = pts[0], last = pts[pts.length - 1];
return ((last - first) / first) * 100;
}
/* ---------- Sparklines (KPI cards) ---------- */
function drawSpark(el, pts) {
var n = pts.length, min = Math.min.apply(null, pts), max = Math.max.apply(null, pts);
var span = max - min || 1;
var coords = pts.map(function (p, i) {
var x = (i / (n - 1)) * 120;
var y = 34 - ((p - min) / span) * 30 - 2;
return x.toFixed(1) + "," + y.toFixed(1);
});
el.setAttribute("points", coords.join(" "));
}
/* ---------- Main trend chart ---------- */
var W = 640, H = 240, PAD = 12;
function toPoints(coords) {
return coords.map(function (c) { return c.x.toFixed(1) + "," + c.y.toFixed(1); }).join(" ");
}
function drawChart(range) {
var nDays = range;
var current = series("mrr", nDays, range);
var previous = series("mrr", nDays, range + 999).map(function (v) { return v * 0.86; });
var all = current.concat(previous);
var min = Math.min.apply(null, all), max = Math.max.apply(null, all);
var pad = (max - min) * 0.15 || 1; min -= pad; max += pad;
var span = max - min || 1;
function proj(pts) {
return pts.map(function (p, i) {
return { x: PAD + (i / (pts.length - 1)) * (W - PAD * 2), y: PAD + (1 - (p - min) / span) * (H - PAD * 2), v: p };
});
}
var A = proj(current), B = proj(previous);
document.getElementById("lineA").setAttribute("points", toPoints(A));
document.getElementById("lineB").setAttribute("points", toPoints(B));
var area = "M" + A[0].x.toFixed(1) + "," + A[0].y.toFixed(1) + " " +
A.slice(1).map(function (c) { return "L" + c.x.toFixed(1) + "," + c.y.toFixed(1); }).join(" ") +
" L" + A[A.length - 1].x.toFixed(1) + "," + (H - PAD) + " L" + A[0].x.toFixed(1) + "," + (H - PAD) + " Z";
document.getElementById("areaA").setAttribute("d", area);
// gridlines
var g = document.getElementById("gridlines");
g.innerHTML = "";
for (var i = 0; i <= 4; i++) {
var y = PAD + (i / 4) * (H - PAD * 2);
var ln = document.createElementNS("http://www.w3.org/2000/svg", "line");
ln.setAttribute("x1", PAD); ln.setAttribute("x2", W - PAD);
ln.setAttribute("y1", y); ln.setAttribute("y2", y);
g.appendChild(ln);
}
chartA = A; chartCurrent = current;
}
var chartA = [], chartCurrent = [];
/* ---------- Chart hover tooltip ---------- */
var chartSvg = document.getElementById("chart");
var dot = document.getElementById("dot");
var tip = document.getElementById("tip");
chartSvg.addEventListener("mousemove", function (e) {
if (!chartA.length) return;
var rect = chartSvg.getBoundingClientRect();
var rx = (e.clientX - rect.left) / rect.width * W;
var idx = Math.round((rx - PAD) / (W - PAD * 2) * (chartA.length - 1));
idx = Math.max(0, Math.min(chartA.length - 1, idx));
var c = chartA[idx];
dot.setAttribute("cx", c.x); dot.setAttribute("cy", c.y); dot.style.display = "";
tip.hidden = false;
tip.textContent = money(c.v);
tip.style.left = (c.x / W * rect.width) + "px";
tip.style.top = (c.y / H * rect.height) + "px";
});
chartSvg.addEventListener("mouseleave", function () {
dot.style.display = "none"; tip.hidden = true;
});
/* ---------- Render everything for a range ---------- */
var currentRange = 30;
function render() {
var range = currentRange;
document.getElementById("chartSub").textContent = "Last " + range + " days";
document.querySelectorAll(".kpi").forEach(function (card) {
var key = card.getAttribute("data-kpi");
var pts = series(key, range, range);
var last = pts[pts.length - 1];
card.querySelector("[data-value]").textContent = KPIS[key].fmt(last);
var d = deltaPct(pts);
var deltaEl = card.querySelector("[data-delta]");
var goodUp = KPIS[key].trend > 0;
var positive = goodUp ? d >= 0 : d < 0;
deltaEl.textContent = (d >= 0 ? "+" : "") + d.toFixed(1) + "%";
deltaEl.classList.toggle("up", positive);
deltaEl.classList.toggle("down", !positive);
drawSpark(card.querySelector("[data-spark]"), pts);
});
drawChart(range);
}
/* ---------- Range switch ---------- */
document.querySelectorAll(".range-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
document.querySelectorAll(".range-btn").forEach(function (b) {
b.classList.remove("active"); b.setAttribute("aria-selected", "false");
});
btn.classList.add("active"); btn.setAttribute("aria-selected", "true");
currentRange = parseInt(btn.getAttribute("data-range"), 10);
render();
toast("Showing last " + currentRange + " days");
});
});
/* ---------- Onboarding nudge ---------- */
var nudge = document.getElementById("nudge");
document.getElementById("nudgeClose").addEventListener("click", function () {
nudge.classList.add("dismissed");
toast("Onboarding hidden — find it in Settings");
});
// animate ring
(function () {
var ring = document.getElementById("ring");
var pct = 70, circ = 113.1;
requestAnimationFrame(function () {
ring.setAttribute("stroke-dashoffset", (circ * (1 - pct / 100)).toFixed(1));
});
document.getElementById("ringNum").textContent = pct + "%";
})();
/* ---------- Tasks widget ---------- */
var TASKS = [
{ label: "Review Q3 retention cohort", due: "Today", done: false },
{ label: "Approve new pricing experiment", due: "Today", done: false },
{ label: "Reply to 3 support escalations", due: "Tue", done: true },
{ label: "Connect Stripe webhook", due: "Wed", done: false },
{ label: "Publish changelog v2.4", due: "Fri", done: false }
];
var tasksEl = document.getElementById("tasks");
function renderTasks() {
tasksEl.innerHTML = "";
TASKS.forEach(function (t, i) {
var li = document.createElement("li");
li.className = "task" + (t.done ? " done" : "");
li.setAttribute("role", "button");
li.setAttribute("tabindex", "0");
li.setAttribute("aria-pressed", String(t.done));
li.innerHTML =
'<span class="check" aria-hidden="true">' + (t.done ? "✓" : "") + "</span>" +
'<span class="task-label">' + t.label + "</span>" +
'<span class="due">' + t.due + "</span>";
function toggle() {
t.done = !t.done;
renderTasks();
toast(t.done ? "Task completed" : "Task reopened");
}
li.addEventListener("click", toggle);
li.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggle(); }
});
tasksEl.appendChild(li);
});
var done = TASKS.filter(function (t) { return t.done; }).length;
document.getElementById("taskCount").textContent = done;
document.getElementById("taskTotal").textContent = TASKS.length;
}
renderTasks();
/* ---------- Widget menu ---------- */
var menuBtn = document.querySelector(".menu-btn");
var menu = document.querySelector(".menu");
menuBtn.addEventListener("click", function (e) {
e.stopPropagation();
var open = !menu.hidden;
menu.hidden = open;
menuBtn.setAttribute("aria-expanded", String(!open));
});
document.addEventListener("click", function (e) {
if (!menu.hidden && !menu.contains(e.target)) {
menu.hidden = true; menuBtn.setAttribute("aria-expanded", "false");
}
});
menu.querySelectorAll("button").forEach(function (b) {
b.addEventListener("click", function () { menu.hidden = true; menuBtn.setAttribute("aria-expanded", "false"); });
});
/* ---------- Activity feed ---------- */
var FEED = [
{ ic: "💳", t: "<b>Marisol R.</b> upgraded to Growth", m: "8 minutes ago" },
{ ic: "🧑🤝🧑", t: "<b>14 new signups</b> from the product hunt launch", m: "42 minutes ago" },
{ ic: "⚡", t: "Automation <b>Welcome series</b> sent 312 emails", m: "1 hour ago" },
{ ic: "⚠️", t: "Webhook <b>checkout.completed</b> retried successfully", m: "2 hours ago" },
{ ic: "📊", t: "<b>Weekly report</b> generated for the founders channel", m: "Yesterday" }
];
var feedEl = document.getElementById("feed");
FEED.forEach(function (f) {
var li = document.createElement("li");
li.innerHTML =
'<span class="fi" aria-hidden="true">' + f.ic + "</span>" +
'<span><span class="ft">' + f.t + "</span><span class=\"fm\">" + f.m + "</span></span>";
feedEl.appendChild(li);
});
/* ---------- Boot ---------- */
render();
})();<!doctype html>
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Northwind — Dashboard</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&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app">
<!-- Sidebar -->
<aside class="sidebar" aria-label="Primary">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M12 2 3 7v10l9 5 9-5V7l-9-5Zm0 2.3 6.5 3.6L12 11.5 5.5 7.9 12 4.3Zm-7 5.2 6 3.4v6.7l-6-3.3V9.5Zm14 0v6.8l-6 3.3v-6.7l6-3.4Z"/></svg>
</span>
<span class="brand-name">Northwind</span>
</div>
<nav class="nav" aria-label="Sections">
<a class="nav-link active" href="#" aria-current="page"><span class="ni" aria-hidden="true">▦</span>Home</a>
<a class="nav-link" href="#"><span class="ni" aria-hidden="true">📈</span>Analytics</a>
<a class="nav-link" href="#"><span class="ni" aria-hidden="true">👥</span>Customers</a>
<a class="nav-link" href="#"><span class="ni" aria-hidden="true">⚙</span>Automations</a>
<a class="nav-link" href="#"><span class="ni" aria-hidden="true">🧾</span>Billing</a>
</nav>
<div class="plan-card">
<p class="plan-name">Growth plan</p>
<p class="plan-meta">8,420 / 10,000 events</p>
<div class="plan-bar"><span style="width:84%"></span></div>
<button class="btn btn-ghost btn-sm" data-toast="Upgrade flow opened (demo)">Upgrade</button>
</div>
</aside>
<!-- Main -->
<div class="main">
<header class="topbar">
<div class="search" role="search">
<span aria-hidden="true">🔎</span>
<input type="search" aria-label="Search" placeholder="Search anything… ⌘K" />
</div>
<div class="topbar-actions">
<button class="icon-btn" id="themeToggle" aria-label="Toggle theme" title="Toggle theme">
<span class="theme-ico" aria-hidden="true"></span>
</button>
<button class="icon-btn" data-toast="No new notifications" aria-label="Notifications">🔔</button>
<span class="avatar" aria-hidden="true">AR</span>
</div>
</header>
<main class="content" aria-label="Dashboard">
<!-- Greeting + quick actions -->
<section class="greet">
<div>
<h1 id="greeting">Good morning, Ari</h1>
<p class="sub">Here's what's happening across your workspace today.</p>
</div>
<div class="quick-actions" role="group" aria-label="Quick actions">
<button class="btn btn-primary" data-toast="New report draft created">+ New report</button>
<button class="btn btn-ghost" data-toast="Invite teammate dialog opened">Invite</button>
<button class="btn btn-ghost" data-toast="Export queued — we'll email you">Export</button>
</div>
</section>
<!-- Onboarding nudge -->
<section class="nudge" id="nudge" aria-label="Onboarding progress">
<div class="nudge-ring" aria-hidden="true">
<svg viewBox="0 0 44 44" width="44" height="44">
<circle cx="22" cy="22" r="18" class="ring-bg"/>
<circle cx="22" cy="22" r="18" class="ring-fg" id="ring" stroke-dasharray="113.1" stroke-dashoffset="33.9"/>
</svg>
<span class="ring-num" id="ringNum">70%</span>
</div>
<div class="nudge-body">
<p class="nudge-title">Finish setting up Northwind</p>
<p class="nudge-text">You're almost there — connect a data source to unlock live metrics.</p>
</div>
<button class="btn btn-primary btn-sm" data-toast="Setup wizard launched">Continue setup</button>
<button class="icon-btn nudge-close" id="nudgeClose" aria-label="Dismiss onboarding">✕</button>
</section>
<!-- Toolbar: date range -->
<div class="toolbar">
<h2 class="section-h">Overview</h2>
<div class="range" role="tablist" aria-label="Date range">
<button class="range-btn" role="tab" data-range="7" aria-selected="false">7d</button>
<button class="range-btn active" role="tab" data-range="30" aria-selected="true">30d</button>
<button class="range-btn" role="tab" data-range="90" aria-selected="false">90d</button>
</div>
</div>
<!-- KPI cards -->
<section class="kpis" aria-label="Key metrics">
<article class="card kpi" data-kpi="mrr">
<div class="kpi-top"><span class="kpi-label">MRR</span><span class="delta up" data-delta>+0%</span></div>
<p class="kpi-value" data-value>$0</p>
<svg class="spark" viewBox="0 0 120 36" preserveAspectRatio="none" aria-hidden="true"><polyline data-spark fill="none" stroke="currentColor" stroke-width="2" points=""/></svg>
</article>
<article class="card kpi" data-kpi="users">
<div class="kpi-top"><span class="kpi-label">Active users</span><span class="delta up" data-delta>+0%</span></div>
<p class="kpi-value" data-value>0</p>
<svg class="spark" viewBox="0 0 120 36" preserveAspectRatio="none" aria-hidden="true"><polyline data-spark fill="none" stroke="currentColor" stroke-width="2" points=""/></svg>
</article>
<article class="card kpi" data-kpi="conv">
<div class="kpi-top"><span class="kpi-label">Conversion</span><span class="delta down" data-delta>+0%</span></div>
<p class="kpi-value" data-value>0%</p>
<svg class="spark" viewBox="0 0 120 36" preserveAspectRatio="none" aria-hidden="true"><polyline data-spark fill="none" stroke="currentColor" stroke-width="2" points=""/></svg>
</article>
<article class="card kpi" data-kpi="churn">
<div class="kpi-top"><span class="kpi-label">Churn</span><span class="delta down" data-delta>+0%</span></div>
<p class="kpi-value" data-value>0%</p>
<svg class="spark" viewBox="0 0 120 36" preserveAspectRatio="none" aria-hidden="true"><polyline data-spark fill="none" stroke="currentColor" stroke-width="2" points=""/></svg>
</article>
</section>
<!-- Grid: chart + side widgets -->
<div class="grid">
<!-- Trend chart -->
<section class="card chart-card" aria-label="Revenue trend">
<div class="card-head">
<div>
<h3 class="card-title">Revenue trend</h3>
<p class="card-sub" id="chartSub">Last 30 days</p>
</div>
<div class="legend">
<span class="lg lg-a">This period</span>
<span class="lg lg-b">Previous</span>
</div>
</div>
<div class="chart-wrap">
<svg id="chart" viewBox="0 0 640 240" preserveAspectRatio="none" role="img" aria-label="Revenue line chart">
<defs>
<linearGradient id="fillA" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="var(--brand)" stop-opacity=".28"/>
<stop offset="100%" stop-color="var(--brand)" stop-opacity="0"/>
</linearGradient>
</defs>
<g id="gridlines"></g>
<path id="areaA" fill="url(#fillA)" stroke="none"></path>
<polyline id="lineB" fill="none" stroke="var(--muted)" stroke-width="2" stroke-dasharray="4 4" opacity=".6" points=""></polyline>
<polyline id="lineA" fill="none" stroke="var(--brand)" stroke-width="2.5" points=""></polyline>
<circle id="dot" r="4.5" fill="var(--brand)" stroke="var(--surface)" stroke-width="2" style="display:none"></circle>
</svg>
<div id="tip" class="tip" hidden></div>
</div>
</section>
<!-- Tasks / checklist -->
<section class="card widget" aria-label="Today's tasks">
<div class="card-head">
<h3 class="card-title">Your tasks</h3>
<div class="menu-wrap">
<button class="icon-btn menu-btn" aria-haspopup="true" aria-expanded="false" aria-label="Task options">⋯</button>
<div class="menu" role="menu" hidden>
<button role="menuitem" data-toast="Showing all tasks">Show completed</button>
<button role="menuitem" data-toast="Sorted by due date">Sort by due date</button>
<button role="menuitem" data-toast="Add task dialog opened">Add task</button>
</div>
</div>
</div>
<p class="task-progress"><strong id="taskCount">0</strong> of <span id="taskTotal">0</span> done</p>
<ul class="tasks" id="tasks"></ul>
</section>
<!-- Activity feed -->
<section class="card widget activity" aria-label="Recent activity">
<div class="card-head">
<h3 class="card-title">Recent activity</h3>
<button class="btn btn-ghost btn-sm" data-toast="Opening full activity log">View all</button>
</div>
<ul class="feed" id="feed"></ul>
</section>
</div>
</main>
</div>
</div>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>App Dashboard / Home
An in-app home screen for a fictional analytics product, Northwind. A persistent sidebar, sticky search topbar, and a time-aware greeting frame the workspace. Four KPI cards surface MRR, active users, conversion, and churn — each with a colored delta pill and an inline SVG sparkline — above a primary revenue trend chart that draws the current period against the previous one with a gradient area fill.
The 7d / 30d / 90d range switch is the heart of the interaction: choosing a range deterministically recomputes every KPI, redraws all sparklines, and re-renders the trend chart. Hovering the chart snaps a dot to the nearest day and shows a value tooltip. A circular onboarding-progress nudge animates on load and can be dismissed, the tasks widget lets you check items done (keyboard accessible) with a live progress count, and a widget overflow menu and recent-activity feed round out the layout. A working light/dark theme toggle repaints the shell and chart.
Everything is self-contained vanilla JS — no frameworks, no build, no external images — with landmark roles, aria states, focus-visible rings, and a layout that collapses gracefully to a single column on phones.
Illustrative SaaS UI only — fictional product, metrics, and billing. No real backend.