Dashboard — Tabbed / sectioned
A sectioned analytics dashboard that splits dense reporting into Overview, Traffic, Revenue, and Users tabs behind a persistent header and sidebar. Built on the WAI-ARIA tabs pattern with arrow-key roving focus, an animated underline that tracks the active tab, and a soft fade between panels. Each section lazy-renders its own widget set with a shimmer placeholder the first time, remembers the last tab, ticks live KPIs, filters by date range, and lets you drag KPI cards to rearrange.
MCP
Code
: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);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
font-size: 14px;
}
:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
border-radius: 6px;
}
/* ───────────────── Layout shell ───────────────── */
.app {
display: grid;
grid-template-columns: 252px 1fr;
min-height: 100vh;
}
/* ───────────────── Sidebar ───────────────── */
.sidebar {
background: var(--white);
border-right: 1px solid var(--line);
padding: 22px 16px;
display: flex;
flex-direction: column;
gap: 24px;
position: sticky;
top: 0;
height: 100vh;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
padding: 4px 8px;
}
.brand-mark {
width: 34px;
height: 34px;
border-radius: 10px;
background: linear-gradient(135deg, var(--brand), var(--brand-700));
color: #fff;
display: grid;
place-items: center;
font-size: 19px;
box-shadow: var(--sh-1);
}
.brand-name {
font-weight: 800;
font-size: 16px;
letter-spacing: -0.2px;
}
.nav-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 2px;
}
.nav-item {
display: flex;
align-items: center;
gap: 11px;
padding: 10px 12px;
border-radius: var(--r-sm);
color: var(--ink-2);
text-decoration: none;
font-weight: 500;
font-size: 13.5px;
transition: background 0.15s, color 0.15s;
}
.nav-item:hover {
background: var(--bg);
color: var(--ink);
}
.nav-item.is-active {
background: var(--brand-50);
color: var(--brand-d);
font-weight: 600;
}
.nav-ico {
font-size: 15px;
width: 18px;
text-align: center;
}
.nav-foot {
margin-top: auto;
}
.plan-card {
background: linear-gradient(150deg, var(--brand-50), #fff);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px;
}
.plan-label {
font-weight: 700;
font-size: 13px;
}
.plan-meta {
color: var(--muted);
font-size: 12px;
margin: 2px 0 9px;
}
.plan-bar {
height: 7px;
border-radius: 99px;
background: rgba(91, 91, 240, 0.16);
overflow: hidden;
}
.plan-bar span {
display: block;
height: 100%;
border-radius: 99px;
background: linear-gradient(90deg, var(--brand), var(--accent));
}
/* ───────────────── Topbar (persistent header) ───────────────── */
.main-wrap {
display: flex;
flex-direction: column;
min-width: 0;
}
.topbar {
position: sticky;
top: 0;
z-index: 20;
background: rgba(246, 247, 251, 0.85);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--line);
display: flex;
align-items: center;
gap: 16px;
padding: 16px 28px;
}
.nav-toggle {
display: none;
border: 1px solid var(--line);
background: var(--white);
border-radius: var(--r-sm);
width: 38px;
height: 38px;
font-size: 17px;
cursor: pointer;
color: var(--ink);
}
.head-text h1 {
font-size: 19px;
font-weight: 800;
letter-spacing: -0.3px;
}
.head-sub {
color: var(--muted);
font-size: 12.5px;
}
.head-tools {
margin-left: auto;
display: flex;
align-items: center;
gap: 12px;
}
.range {
display: inline-flex;
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 3px;
box-shadow: var(--sh-1);
}
.range-btn {
border: 0;
background: transparent;
padding: 6px 12px;
border-radius: 6px;
font: inherit;
font-size: 12.5px;
font-weight: 600;
color: var(--muted);
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.range-btn:hover {
color: var(--ink);
}
.range-btn.is-active {
background: var(--brand);
color: #fff;
}
.live-toggle {
display: inline-flex;
align-items: center;
gap: 7px;
border: 1px solid var(--line);
background: var(--white);
border-radius: var(--r-sm);
padding: 7px 12px;
font: inherit;
font-size: 12.5px;
font-weight: 600;
color: var(--ink-2);
cursor: pointer;
box-shadow: var(--sh-1);
}
.live-dot {
width: 8px;
height: 8px;
border-radius: 99px;
background: var(--ok);
box-shadow: 0 0 0 0 rgba(47, 158, 111, 0.5);
animation: pulse 1.8s infinite;
}
.live-toggle[aria-pressed="false"] .live-dot {
background: var(--muted);
animation: none;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(47, 158, 111, 0.5); }
70% { box-shadow: 0 0 0 7px rgba(47, 158, 111, 0); }
100% { box-shadow: 0 0 0 0 rgba(47, 158, 111, 0); }
}
.avatar {
width: 38px;
height: 38px;
border-radius: 99px;
background: linear-gradient(135deg, var(--accent), var(--brand));
color: #fff;
display: grid;
place-items: center;
font-weight: 700;
font-size: 13px;
}
/* ───────────────── Content + tabs ───────────────── */
.content {
padding: 22px 28px 40px;
}
.tabs {
position: relative;
display: flex;
gap: 4px;
border-bottom: 1px solid var(--line);
margin-bottom: 22px;
}
.tab {
position: relative;
border: 0;
background: transparent;
padding: 11px 16px;
font: inherit;
font-weight: 600;
font-size: 14px;
color: var(--muted);
cursor: pointer;
border-radius: var(--r-sm) var(--r-sm) 0 0;
transition: color 0.15s;
}
.tab:hover {
color: var(--ink-2);
}
.tab[aria-selected="true"] {
color: var(--brand-d);
}
.tab-ink {
position: absolute;
bottom: -1px;
left: 0;
height: 2.5px;
border-radius: 99px;
background: var(--brand);
transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1), width 0.28s cubic-bezier(0.4, 0, 0.2, 1);
}
.panel {
display: none;
animation: fade 0.32s ease;
}
.panel.is-active {
display: block;
}
@keyframes fade {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
/* ───────────────── Grid + cards ───────────────── */
.grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 16px;
}
.card {
grid-column: var(--grid, span 12);
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 18px;
box-shadow: var(--sh-1);
transition: box-shadow 0.18s, border-color 0.18s, transform 0.18s;
}
.card:hover {
box-shadow: var(--sh-2);
border-color: var(--line-2);
}
.card.dragging {
opacity: 0.55;
transform: scale(0.99);
}
.card.drop-target {
outline: 2px dashed var(--brand);
outline-offset: -2px;
}
.card-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
}
.card-title {
font-weight: 700;
font-size: 13.5px;
color: var(--ink-2);
}
.menu {
border: 0;
background: transparent;
color: var(--muted);
font-size: 17px;
line-height: 1;
cursor: grab;
border-radius: 6px;
padding: 2px 6px;
}
.menu:hover {
background: var(--bg);
color: var(--ink);
}
/* KPI cards */
.kpi-value {
font-size: 27px;
font-weight: 800;
letter-spacing: -0.6px;
line-height: 1.1;
}
.kpi-delta {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12.5px;
font-weight: 700;
margin-top: 6px;
}
.kpi-delta span {
color: var(--muted);
font-weight: 500;
}
.kpi-delta.up { color: var(--ok); }
.kpi-delta.down { color: var(--danger); }
.spark {
width: 100%;
height: 30px;
margin-top: 12px;
display: block;
}
.spark polyline {
fill: none;
stroke: var(--brand);
stroke-width: 2.4;
stroke-linecap: round;
stroke-linejoin: round;
vector-effect: non-scaling-stroke;
}
.kpi .kpi-delta.down ~ .spark polyline { stroke: var(--danger); }
/* ───────────────── Bar chart ───────────────── */
.chart-bars {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
align-items: end;
gap: 10px;
height: 190px;
padding-top: 8px;
}
.bar-col {
display: flex;
flex-direction: column;
align-items: center;
gap: 7px;
height: 100%;
justify-content: flex-end;
}
.bar-stack {
position: relative;
width: 60%;
max-width: 30px;
flex: 1;
display: flex;
align-items: flex-end;
}
.bar-fill {
width: 100%;
border-radius: 6px 6px 3px 3px;
background: linear-gradient(180deg, var(--brand), var(--brand-700));
transition: height 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.bar-target {
position: absolute;
left: -3px;
right: -3px;
height: 2px;
background: var(--accent);
border-radius: 2px;
}
.bar-label {
font-size: 11px;
color: var(--muted);
font-weight: 600;
}
.legend {
list-style: none;
display: flex;
gap: 16px;
margin-top: 12px;
font-size: 12px;
color: var(--muted);
font-weight: 600;
}
.legend li {
display: inline-flex;
align-items: center;
gap: 6px;
}
.sw {
width: 11px;
height: 11px;
border-radius: 3px;
display: inline-block;
}
.sw-brand { background: var(--brand); }
.sw-line { background: var(--accent); }
/* ───────────────── Donut ───────────────── */
.donut-row {
display: flex;
align-items: center;
gap: 18px;
}
.donut {
width: 116px;
height: 116px;
flex-shrink: 0;
}
.d-track {
fill: none;
stroke: var(--bg);
stroke-width: 5;
}
.d-seg {
fill: none;
stroke-width: 5;
stroke-linecap: round;
transform: rotate(-90deg);
transform-origin: 50% 50%;
}
.d-seg.s1 { stroke: var(--brand); }
.d-seg.s2 { stroke: var(--accent); }
.d-seg.s3 { stroke: var(--warn); }
.d-seg.s4 { stroke: var(--brand-700); }
.d-center {
font-size: 7px;
font-weight: 800;
fill: var(--ink);
text-anchor: middle;
}
.d-sub {
font-size: 3px;
font-weight: 600;
fill: var(--muted);
text-anchor: middle;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.donut-legend {
list-style: none;
display: flex;
flex-direction: column;
gap: 9px;
font-size: 12.5px;
font-weight: 600;
flex: 1;
}
.donut-legend li {
display: flex;
align-items: center;
gap: 8px;
}
.donut-legend b {
margin-left: auto;
color: var(--ink);
}
.donut-legend .sw.s1 { background: var(--brand); }
.donut-legend .sw.s2 { background: var(--accent); }
.donut-legend .sw.s3 { background: var(--warn); }
.donut-legend .sw.s4 { background: var(--brand-700); }
/* ───────────────── Line chart (svg) ───────────────── */
.line-chart {
width: 100%;
height: 200px;
display: block;
}
.lc-grid {
stroke: var(--line);
stroke-width: 0.5;
}
.lc-area { fill: url(#lcGrad); }
.lc-line {
fill: none;
stroke: var(--brand);
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.lc-dot {
fill: var(--white);
stroke: var(--brand);
stroke-width: 2;
}
/* ───────────────── Table ───────────────── */
.tbl {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.tbl th {
text-align: left;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--muted);
padding: 6px 10px;
border-bottom: 1px solid var(--line);
}
.tbl td {
padding: 11px 10px;
border-bottom: 1px solid var(--line);
color: var(--ink-2);
}
.tbl tr:last-child td {
border-bottom: 0;
}
.tbl .num {
text-align: right;
font-variant-numeric: tabular-nums;
font-weight: 600;
color: var(--ink);
}
.pill {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
font-weight: 700;
padding: 3px 8px;
border-radius: 99px;
}
.pill.up { background: rgba(47, 158, 111, 0.12); color: var(--ok); }
.pill.down { background: rgba(212, 80, 62, 0.12); color: var(--danger); }
.mini-bar {
height: 6px;
border-radius: 99px;
background: var(--bg);
overflow: hidden;
min-width: 80px;
}
.mini-bar span {
display: block;
height: 100%;
border-radius: 99px;
background: linear-gradient(90deg, var(--brand), var(--accent));
}
/* progress / horizontal bars */
.hbar-list {
display: flex;
flex-direction: column;
gap: 14px;
}
.hbar-row {
display: grid;
grid-template-columns: 96px 1fr auto;
align-items: center;
gap: 12px;
font-size: 12.5px;
}
.hbar-row b {
text-align: right;
font-variant-numeric: tabular-nums;
}
.hbar {
height: 9px;
border-radius: 99px;
background: var(--bg);
overflow: hidden;
}
.hbar span {
display: block;
height: 100%;
border-radius: 99px;
background: linear-gradient(90deg, var(--brand), var(--brand-d));
}
/* ───────────────── Shimmer (lazy load) ───────────────── */
.skeleton {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 16px;
}
.sk {
grid-column: var(--grid, span 12);
border-radius: var(--r-lg);
background: var(--surface);
border: 1px solid var(--line);
padding: 18px;
}
.sk-line {
height: 14px;
border-radius: 6px;
background: linear-gradient(90deg, var(--bg) 25%, #eceef6 37%, var(--bg) 63%);
background-size: 400% 100%;
animation: shimmer 1.3s ease infinite;
margin-bottom: 12px;
}
.sk-line.lg { height: 30px; width: 60%; }
.sk-block {
height: 150px;
border-radius: var(--r-md);
background: linear-gradient(90deg, var(--bg) 25%, #eceefe 37%, var(--bg) 63%);
background-size: 400% 100%;
animation: shimmer 1.3s ease infinite;
}
@keyframes shimmer {
0% { background-position: 100% 0; }
100% { background-position: 0 0; }
}
/* ───────────────── Toast ───────────────── */
.toast {
position: fixed;
bottom: 22px;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: var(--ink);
color: #fff;
padding: 11px 18px;
border-radius: var(--r-sm);
font-size: 13px;
font-weight: 600;
box-shadow: var(--sh-2);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s, transform 0.25s;
z-index: 100;
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* ───────────────── Responsive ───────────────── */
@media (max-width: 980px) {
.grid { grid-template-columns: repeat(6, 1fr); }
.card[style*="span 8"] { grid-column: span 6 !important; }
.card[style*="span 4"] { grid-column: span 6 !important; }
.card[style*="span 3"] { grid-column: span 3 !important; }
}
@media (max-width: 720px) {
.app { grid-template-columns: 1fr; }
.sidebar {
position: fixed;
z-index: 60;
left: 0;
top: 0;
width: 260px;
transform: translateX(-100%);
transition: transform 0.25s ease;
box-shadow: var(--sh-2);
}
.sidebar.open { transform: translateX(0); }
.nav-toggle { display: grid; place-items: center; }
.content { padding: 18px 16px 36px; }
.topbar { padding: 14px 16px; }
.head-tools { gap: 8px; }
.grid, .skeleton { grid-template-columns: repeat(2, 1fr); gap: 12px; }
.card[style*="span 8"], .card[style*="span 4"] { grid-column: span 2 !important; }
.card[style*="span 3"] { grid-column: span 1 !important; }
.head-text h1 { font-size: 17px; }
.range-btn[data-range="90d"], .range-btn[data-range="12m"] { display: none; }
}
@media (max-width: 420px) {
.grid, .skeleton { grid-template-columns: 1fr; }
.card[style*="span 3"] { grid-column: span 1 !important; }
.donut-row { flex-direction: column; align-items: flex-start; }
.live-toggle span:not(.live-dot) { display: none; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
}/* Lumio / Northwind — Tabbed dashboard
Vanilla JS: ARIA tabs (arrow-key nav + roving tabindex), fade panel swap,
lazy panel "load" with shimmer, last-tab memory, live ticking KPIs,
date-range filter, draggable KPI cards, toast helper. No libraries. */
(function () {
"use strict";
var STORE_KEY = "lumio.lastTab";
var rangeFactor = { "7d": 0.42, "30d": 1, "90d": 2.7, "12m": 9.4 };
/* ───────── Toast helper ───────── */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2200);
}
/* ───────── Number formatting ───────── */
function fmt(value, prefix, suffix, decimals) {
var n = decimals
? value.toFixed(decimals)
: Math.round(value).toLocaleString("en-US");
if (decimals) n = Number(n).toLocaleString("en-US", {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
});
return (prefix || "") + n + (suffix || "");
}
/* ───────── Tabs (ARIA pattern) ───────── */
var tablist = document.querySelector('[role="tablist"]');
var tabs = Array.prototype.slice.call(document.querySelectorAll('[role="tab"]'));
var ink = document.querySelector(".tab-ink");
var loaded = {}; // panelId -> true once lazily built
function moveInk(tab) {
if (!ink || !tab) return;
ink.style.width = tab.offsetWidth + "px";
ink.style.transform = "translateX(" + tab.offsetLeft + "px)";
}
function panelFor(tab) {
return document.getElementById(tab.getAttribute("aria-controls"));
}
function selectTab(tab, focus) {
tabs.forEach(function (t) {
var selected = t === tab;
t.setAttribute("aria-selected", selected ? "true" : "false");
t.tabIndex = selected ? 0 : -1;
var p = panelFor(t);
if (p) {
p.classList.toggle("is-active", selected);
if (selected) {
p.hidden = false;
} else {
p.hidden = true;
}
}
});
if (focus) tab.focus();
moveInk(tab);
var panel = panelFor(tab);
if (panel) lazyLoad(panel);
try { localStorage.setItem(STORE_KEY, tab.id); } catch (e) {}
}
tabs.forEach(function (tab) {
tab.addEventListener("click", function () { selectTab(tab, false); });
tab.addEventListener("keydown", function (e) {
var i = tabs.indexOf(tab);
var next = null;
switch (e.key) {
case "ArrowRight":
case "ArrowDown":
next = tabs[(i + 1) % tabs.length]; break;
case "ArrowLeft":
case "ArrowUp":
next = tabs[(i - 1 + tabs.length) % tabs.length]; break;
case "Home":
next = tabs[0]; break;
case "End":
next = tabs[tabs.length - 1]; break;
default:
return;
}
e.preventDefault();
selectTab(next, true);
});
});
/* ───────── Lazy panel loading with shimmer ───────── */
function lazyLoad(panel) {
if (loaded[panel.id]) return;
loaded[panel.id] = true;
var builder = builders[panel.id];
if (!builder) return; // Overview is pre-rendered in HTML
// Show skeleton shimmer first
panel.innerHTML = skeleton();
setTimeout(function () {
panel.innerHTML = builder();
hydratePanel(panel);
}, 620);
}
function skeleton() {
function sk(grid, block) {
return '<div class="sk" style="--grid:span ' + grid + '">' +
'<div class="sk-line"></div>' +
'<div class="sk-line lg"></div>' +
(block ? '<div class="sk-block"></div>' : '') +
"</div>";
}
return '<div class="skeleton">' +
sk(3) + sk(3) + sk(3) + sk(3) +
sk(8, true) + sk(4, true) +
"</div>";
}
/* ───────── Reusable widget markup ───────── */
function kpiCard(title, value, delta, dir, points) {
return '<article class="card kpi" style="--grid:span 3">' +
'<header class="card-head"><span class="card-title">' + title +
'</span><button class="menu" aria-label="Widget options">⋯</button></header>' +
'<p class="kpi-value">' + value + "</p>" +
'<p class="kpi-delta ' + dir + '">' + (dir === "up" ? "▲" : "▼") + " " +
delta + ' <span>vs prev</span></p>' +
'<svg class="spark" viewBox="0 0 100 28" preserveAspectRatio="none" aria-hidden="true">' +
'<polyline points="' + points + '" /></svg></article>';
}
function barChart(rows, max) {
var cols = rows.map(function (r) {
var h = Math.round((r.v / max) * 100);
var t = Math.round((r.t / max) * 100);
return '<div class="bar-col"><div class="bar-stack">' +
'<div class="bar-fill" style="height:0%" data-h="' + h + '"></div>' +
'<div class="bar-target" style="bottom:' + t + '%"></div></div>' +
'<span class="bar-label">' + r.l + "</span></div>";
}).join("");
return '<div class="chart-bars">' + cols + "</div>" +
'<ul class="legend"><li><i class="sw sw-brand"></i>Value</li>' +
'<li><i class="sw sw-line"></i>Target</li></ul>';
}
function lineChart(points, area) {
return '<svg class="line-chart" viewBox="0 0 460 200" role="img" aria-label="Trend over time">' +
'<defs><linearGradient id="lcGrad" x1="0" y1="0" x2="0" y2="1">' +
'<stop offset="0%" stop-color="var(--brand)" stop-opacity="0.22"/>' +
'<stop offset="100%" stop-color="var(--brand)" stop-opacity="0"/></linearGradient></defs>' +
'<line class="lc-grid" x1="0" y1="50" x2="460" y2="50"/>' +
'<line class="lc-grid" x1="0" y1="100" x2="460" y2="100"/>' +
'<line class="lc-grid" x1="0" y1="150" x2="460" y2="150"/>' +
'<path class="lc-area" d="' + area + '"/>' +
'<polyline class="lc-line" points="' + points + '"/>' +
"</svg>";
}
function hbarList(rows) {
return '<div class="hbar-list">' + rows.map(function (r) {
return '<div class="hbar-row"><span>' + r.l + "</span>" +
'<div class="hbar"><span style="width:' + r.p + '%"></span></div>' +
"<b>" + r.v + "</b></div>";
}).join("") + "</div>";
}
function donut(center, sub, segs, legend) {
var off = 25, circles = "";
segs.forEach(function (s, i) {
circles += '<circle class="d-seg s' + (i + 1) + '" cx="21" cy="21" r="15.9" ' +
'stroke-dasharray="' + s + " " + (100 - s) + '" stroke-dashoffset="' + off + '"></circle>';
off = (off - s + 100) % 100;
});
var legendHtml = legend.map(function (g, i) {
return '<li><i class="sw s' + (i + 1) + '"></i>' + g.l + "<b>" + g.v + "</b></li>";
}).join("");
return '<div class="donut-row"><svg class="donut" viewBox="0 0 42 42" role="img" aria-label="Breakdown chart">' +
'<circle class="d-track" cx="21" cy="21" r="15.9"></circle>' + circles +
'<text class="d-center" x="21" y="20">' + center + "</text>" +
'<text class="d-sub" x="21" y="25">' + sub + "</text></svg>" +
'<ul class="donut-legend">' + legendHtml + "</ul></div>";
}
function card(title, span, body) {
return '<article class="card" style="--grid:span ' + span + '">' +
'<header class="card-head"><span class="card-title">' + title +
'</span><button class="menu" aria-label="Widget options">⋯</button></header>' +
body + "</article>";
}
function grid(inner) { return '<div class="grid">' + inner + "</div>"; }
/* ───────── Panel builders (lazy) ───────── */
var builders = {
"panel-traffic": function () {
return grid(
kpiCard("Visitors", "61,204", "9.8%", "up", "0,22 14,17 28,19 42,12 56,14 70,7 84,9 100,4") +
kpiCard("Pageviews", "248,910", "5.4%", "up", "0,20 14,18 28,15 42,16 56,11 70,12 84,8 100,6") +
kpiCard("Bounce rate", "38.4%", "2.1%", "down", "0,8 14,9 28,11 42,12 56,14 70,15 84,17 100,19") +
kpiCard("Avg. session", "3m 12s", "4.0%", "up", "0,21 14,18 28,17 42,13 56,14 70,10 84,11 100,7") +
card("Sessions by day", 8,
barChart([
{ l: "Mon", v: 54, t: 60 }, { l: "Tue", v: 72, t: 60 },
{ l: "Wed", v: 61, t: 60 }, { l: "Thu", v: 88, t: 60 },
{ l: "Fri", v: 96, t: 60 }, { l: "Sat", v: 43, t: 60 },
{ l: "Sun", v: 38, t: 60 }
], 100)) +
card("Top pages", 4,
hbarList([
{ l: "/pricing", p: 92, v: "48.2k" },
{ l: "/features", p: 74, v: "38.9k" },
{ l: "/blog/launch", p: 58, v: "30.4k" },
{ l: "/docs/start", p: 41, v: "21.6k" },
{ l: "/changelog", p: 29, v: "15.1k" }
]))
);
},
"panel-revenue": function () {
return grid(
kpiCard("MRR", "$96,420", "6.9%", "up", "0,24 14,21 28,19 42,16 56,15 70,11 84,9 100,5") +
kpiCard("Net churn", "2.1%", "0.4%", "down", "0,15 14,14 28,16 42,13 56,15 70,12 84,14 100,11") +
kpiCard("LTV", "$1,284", "3.2%", "up", "0,22 14,20 28,18 42,17 56,14 70,13 84,10 100,7") +
kpiCard("Expansion", "$11,930", "8.1%", "up", "0,23 14,20 28,21 42,15 56,16 70,12 84,10 100,6") +
card("Revenue by plan", 8,
barChart([
{ l: "Starter", v: 36, t: 40 }, { l: "Team", v: 64, t: 60 },
{ l: "Business", v: 90, t: 70 }, { l: "Scale", v: 71, t: 65 },
{ l: "Enterprise", v: 48, t: 55 }
], 100)) +
card("Channel revenue", 4,
donut("$96k", "MRR",
[42, 27, 18, 13],
[{ l: "Direct", v: "42%" }, { l: "Partner", v: "27%" },
{ l: "Self-serve", v: "18%" }, { l: "Reseller", v: "13%" }])) +
card("Recent invoices", 12,
'<table class="tbl"><thead><tr><th>Account</th><th>Plan</th>' +
'<th class="num">Amount</th><th>Status</th></tr></thead><tbody>' +
'<tr><td>Brightside Labs</td><td>Business</td><td class="num">$1,490</td><td><span class="pill up">Paid</span></td></tr>' +
'<tr><td>Atlas Robotics</td><td>Scale</td><td class="num">$3,200</td><td><span class="pill up">Paid</span></td></tr>' +
'<tr><td>Quartz Media</td><td>Enterprise</td><td class="num">$8,750</td><td><span class="pill up">Paid</span></td></tr>' +
'<tr><td>Fern & Oak</td><td>Starter</td><td class="num">$120</td><td><span class="pill down">Failed</span></td></tr>' +
"</tbody></table>")
);
},
"panel-users": function () {
return grid(
kpiCard("Active users", "54,310", "9.3%", "up", "0,24 14,20 28,21 42,15 56,16 70,10 84,11 100,4") +
kpiCard("New signups", "3,206", "14.0%", "up", "0,26 14,20 28,22 42,14 56,12 70,9 84,11 100,3") +
kpiCard("Retention 30d", "76.5%", "1.6%", "up", "0,14 14,13 28,12 42,11 56,10 70,9 84,8 100,6") +
kpiCard("Activation", "61.2%", "0.9%", "down", "0,9 14,10 28,9 42,11 56,12 70,13 84,12 100,14") +
card("Signups (12 weeks)", 8,
lineChart(
"0,150 42,140 84,148 126,120 168,128 210,96 252,104 294,72 336,80 378,52 420,60 460,36",
"M0,150 L42,140 L84,148 L126,120 L168,128 L210,96 L252,104 L294,72 L336,80 L378,52 L420,60 L460,36 L460,200 L0,200 Z")) +
card("Device split", 4,
donut("54.3k", "users",
[58, 31, 11],
[{ l: "Mobile", v: "58%" }, { l: "Desktop", v: "31%" }, { l: "Tablet", v: "11%" }])) +
card("Users by region", 12,
hbarList([
{ l: "North America", p: 88, v: "21.4k" },
{ l: "Europe", p: 69, v: "16.8k" },
{ l: "Asia Pacific", p: 52, v: "12.7k" },
{ l: "LATAM", p: 24, v: "5.9k" }
]))
);
}
};
/* ───────── Post-build hydration (animate bars, wire menus/drag) ───────── */
function hydratePanel(panel) {
// animate bar fills
requestAnimationFrame(function () {
panel.querySelectorAll(".bar-fill").forEach(function (b) {
b.style.height = (b.getAttribute("data-h") || 0) + "%";
});
});
enableDrag(panel);
wireMenus(panel);
applyRange(currentRange, panel);
}
/* ───────── Date range filter ───────── */
var currentRange = "30d";
var rangeBtns = Array.prototype.slice.call(document.querySelectorAll(".range-btn"));
function applyRange(range, scope) {
var factor = rangeFactor[range] || 1;
var root = scope || document;
root.querySelectorAll(".kpi-value[data-base]").forEach(function (el) {
var base = parseFloat(el.getAttribute("data-base"));
if (isNaN(base)) return;
var prefix = el.getAttribute("data-prefix") || "";
var suffix = el.getAttribute("data-suffix") || "";
var decimals = base % 1 !== 0 ? 2 : 0;
// Rates (%) and averages (sub-100 decimals) don't scale with the window;
// only cumulative totals/counts do.
var isRate = suffix === "%" || (base % 1 !== 0 && base < 100);
var scaled = isRate ? base : base * factor;
el.textContent = fmt(scaled, prefix, suffix, decimals);
});
}
rangeBtns.forEach(function (btn) {
btn.addEventListener("click", function () {
rangeBtns.forEach(function (b) {
b.classList.remove("is-active");
b.setAttribute("aria-pressed", "false");
});
btn.classList.add("is-active");
btn.setAttribute("aria-pressed", "true");
currentRange = btn.getAttribute("data-range");
applyRange(currentRange);
toast("Range set to " + btn.textContent.trim());
});
});
/* ───────── Live ticking KPIs ───────── */
var liveToggle = document.getElementById("liveToggle");
var liveTimer;
function tickLive() {
var active = document.querySelector(".panel.is-active");
if (!active) return;
active.querySelectorAll(".kpi-value[data-base]").forEach(function (el) {
if (Math.random() > 0.5) return;
var base = parseFloat(el.getAttribute("data-base"));
if (isNaN(base)) return;
var drift = base * (Math.random() * 0.004 - 0.0015);
base = Math.max(0, base + drift);
el.setAttribute("data-base", base);
});
applyRange(currentRange, active);
}
function startLive() { liveTimer = setInterval(tickLive, 2600); }
function stopLive() { clearInterval(liveTimer); }
if (liveToggle) {
startLive();
liveToggle.addEventListener("click", function () {
var on = liveToggle.getAttribute("aria-pressed") === "true";
liveToggle.setAttribute("aria-pressed", on ? "false" : "true");
if (on) { stopLive(); toast("Live updates paused"); }
else { startLive(); toast("Live updates resumed"); }
});
}
/* ───────── Drag to rearrange KPI cards ───────── */
function enableDrag(scope) {
var root = scope || document;
root.querySelectorAll(".card.kpi").forEach(function (card) {
if (card.dataset.dragReady) return;
card.dataset.dragReady = "1";
card.setAttribute("draggable", "true");
card.addEventListener("dragstart", function (e) {
card.classList.add("dragging");
e.dataTransfer.effectAllowed = "move";
try { e.dataTransfer.setData("text/plain", "card"); } catch (err) {}
});
card.addEventListener("dragend", function () {
card.classList.remove("dragging");
document.querySelectorAll(".drop-target").forEach(function (c) {
c.classList.remove("drop-target");
});
toast("Layout updated");
});
card.addEventListener("dragover", function (e) {
e.preventDefault();
if (!card.classList.contains("dragging")) card.classList.add("drop-target");
});
card.addEventListener("dragleave", function () {
card.classList.remove("drop-target");
});
card.addEventListener("drop", function (e) {
e.preventDefault();
card.classList.remove("drop-target");
var dragging = root.querySelector(".card.dragging");
if (!dragging || dragging === card) return;
var grid = card.parentNode;
var cards = Array.prototype.slice.call(grid.children);
if (cards.indexOf(dragging) < cards.indexOf(card)) {
grid.insertBefore(dragging, card.nextSibling);
} else {
grid.insertBefore(dragging, card);
}
});
});
}
/* ───────── Widget menu (⋯) ───────── */
function wireMenus(scope) {
(scope || document).querySelectorAll(".menu").forEach(function (m) {
if (m.dataset.wired) return;
m.dataset.wired = "1";
m.addEventListener("click", function () {
var title = m.closest(".card").querySelector(".card-title");
toast((title ? title.textContent : "Widget") + " options");
});
});
}
/* ───────── Mobile nav toggle ───────── */
var navToggle = document.getElementById("navToggle");
var sidebar = document.querySelector(".sidebar");
if (navToggle && sidebar) {
navToggle.setAttribute("aria-expanded", "false");
navToggle.addEventListener("click", function () {
var open = sidebar.classList.toggle("open");
navToggle.setAttribute("aria-expanded", open ? "true" : "false");
});
document.addEventListener("click", function (e) {
if (window.innerWidth > 720) return;
if (sidebar.contains(e.target) || navToggle.contains(e.target)) return;
sidebar.classList.remove("open");
navToggle.setAttribute("aria-expanded", "false");
});
}
/* ───────── Overview bar chart (#ovBars) ───────── */
var ovBars = document.getElementById("ovBars");
if (ovBars) {
var ovData = [
{ l: "W1", v: 58, t: 60 }, { l: "W2", v: 66, t: 60 },
{ l: "W3", v: 54, t: 62 }, { l: "W4", v: 78, t: 64 },
{ l: "W5", v: 71, t: 66 }, { l: "W6", v: 89, t: 68 },
{ l: "W7", v: 82, t: 70 }, { l: "W8", v: 96, t: 72 }
];
ovBars.innerHTML = ovData.map(function (r) {
return '<div class="bar-col"><div class="bar-stack">' +
'<div class="bar-fill" style="height:0%" data-h="' + r.v + '"></div>' +
'<div class="bar-target" style="bottom:' + r.t + '%"></div></div>' +
'<span class="bar-label">' + r.l + "</span></div>";
}).join("");
requestAnimationFrame(function () {
ovBars.querySelectorAll(".bar-fill").forEach(function (b) {
b.style.height = b.getAttribute("data-h") + "%";
});
});
}
/* ───────── Init ───────── */
enableDrag(document);
wireMenus(document);
applyRange(currentRange);
var startTab = tabs[0];
try {
var saved = localStorage.getItem(STORE_KEY);
if (saved) {
var savedTab = tabs.filter(function (t) { return t.id === saved; })[0];
if (savedTab) startTab = savedTab;
}
} catch (e) {}
// Mark overview as loaded since it's in the HTML already
loaded["panel-overview"] = true;
selectTab(startTab, false);
window.addEventListener("load", function () { moveInk(startTab); });
window.addEventListener("resize", function () {
var sel = document.querySelector('[role="tab"][aria-selected="true"]');
moveInk(sel);
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Northwind Analytics — Tabbed 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;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app">
<!-- ───────────────── Sidebar ───────────────── -->
<nav class="sidebar" aria-label="Primary">
<div class="brand">
<span class="brand-mark" aria-hidden="true">◑</span>
<span class="brand-name">Northwind</span>
</div>
<ul class="nav-list">
<li><a href="#" class="nav-item is-active" aria-current="page"><span class="nav-ico" aria-hidden="true">▦</span>Dashboard</a></li>
<li><a href="#" class="nav-item"><span class="nav-ico" aria-hidden="true">◷</span>Reports</a></li>
<li><a href="#" class="nav-item"><span class="nav-ico" aria-hidden="true">⚲</span>Segments</a></li>
<li><a href="#" class="nav-item"><span class="nav-ico" aria-hidden="true">◈</span>Campaigns</a></li>
<li><a href="#" class="nav-item"><span class="nav-ico" aria-hidden="true">⚙</span>Settings</a></li>
</ul>
<div class="nav-foot">
<div class="plan-card">
<p class="plan-label">Growth plan</p>
<p class="plan-meta">82% of quota used</p>
<div class="plan-bar"><span style="width:82%"></span></div>
</div>
</div>
</nav>
<!-- ───────────────── Main ───────────────── -->
<div class="main-wrap">
<!-- Persistent header -->
<header class="topbar">
<button class="nav-toggle" id="navToggle" aria-label="Toggle navigation">☰</button>
<div class="head-text">
<h1>Performance overview</h1>
<p class="head-sub">Acme Cloud · all properties · fictional data</p>
</div>
<div class="head-tools">
<div class="range" role="group" aria-label="Date range">
<button class="range-btn" data-range="7d">7d</button>
<button class="range-btn is-active" data-range="30d" aria-pressed="true">30d</button>
<button class="range-btn" data-range="90d">90d</button>
<button class="range-btn" data-range="12m">12m</button>
</div>
<button class="live-toggle" id="liveToggle" aria-pressed="true">
<span class="live-dot" aria-hidden="true"></span>Live
</button>
<div class="avatar" title="Dana Okafor" aria-hidden="true">DO</div>
</div>
</header>
<main class="content" id="content">
<!-- Tab bar -->
<div class="tabs" role="tablist" aria-label="Dashboard sections">
<button role="tab" id="tab-overview" aria-controls="panel-overview" aria-selected="true" tabindex="0" class="tab">Overview</button>
<button role="tab" id="tab-traffic" aria-controls="panel-traffic" aria-selected="false" tabindex="-1" class="tab">Traffic</button>
<button role="tab" id="tab-revenue" aria-controls="panel-revenue" aria-selected="false" tabindex="-1" class="tab">Revenue</button>
<button role="tab" id="tab-users" aria-controls="panel-users" aria-selected="false" tabindex="-1" class="tab">Users</button>
<span class="tab-ink" aria-hidden="true"></span>
</div>
<!-- ───── Panel: Overview ───── -->
<section role="tabpanel" id="panel-overview" aria-labelledby="tab-overview" class="panel is-active" tabindex="0">
<div class="grid">
<article class="card kpi" style="--grid:span 3">
<header class="card-head"><span class="card-title">Revenue</span><button class="menu" aria-label="Widget options">⋯</button></header>
<p class="kpi-value" data-base="184320" data-prefix="$">$184,320</p>
<p class="kpi-delta up">▲ 12.4% <span>vs prev</span></p>
<svg class="spark" viewBox="0 0 100 28" preserveAspectRatio="none" aria-hidden="true"><polyline points="0,22 14,18 28,20 42,12 56,15 70,8 84,10 100,4" /></svg>
</article>
<article class="card kpi" style="--grid:span 3">
<header class="card-head"><span class="card-title">Sessions</span><button class="menu" aria-label="Widget options">⋯</button></header>
<p class="kpi-value" data-base="92840">92,840</p>
<p class="kpi-delta up">▲ 6.1% <span>vs prev</span></p>
<svg class="spark" viewBox="0 0 100 28" preserveAspectRatio="none" aria-hidden="true"><polyline points="0,18 14,20 28,14 42,16 56,10 70,13 84,7 100,9" /></svg>
</article>
<article class="card kpi" style="--grid:span 3">
<header class="card-head"><span class="card-title">Conversion</span><button class="menu" aria-label="Widget options">⋯</button></header>
<p class="kpi-value" data-base="3.42" data-suffix="%">3.42%</p>
<p class="kpi-delta down">▼ 0.3% <span>vs prev</span></p>
<svg class="spark" viewBox="0 0 100 28" preserveAspectRatio="none" aria-hidden="true"><polyline points="0,8 14,10 28,7 42,12 56,11 70,15 84,13 100,18" /></svg>
</article>
<article class="card kpi" style="--grid:span 3">
<header class="card-head"><span class="card-title">Avg. order</span><button class="menu" aria-label="Widget options">⋯</button></header>
<p class="kpi-value" data-base="68.9" data-prefix="$">$68.90</p>
<p class="kpi-delta up">▲ 2.7% <span>vs prev</span></p>
<svg class="spark" viewBox="0 0 100 28" preserveAspectRatio="none" aria-hidden="true"><polyline points="0,20 14,16 28,17 42,11 56,13 70,9 84,11 100,6" /></svg>
</article>
<article class="card" style="--grid:span 8">
<header class="card-head"><span class="card-title">Revenue vs target</span><button class="menu" aria-label="Widget options">⋯</button></header>
<div class="chart-bars" id="ovBars" aria-hidden="true"></div>
<ul class="legend"><li><i class="sw sw-brand"></i>Revenue</li><li><i class="sw sw-line"></i>Target</li></ul>
</article>
<article class="card" style="--grid:span 4">
<header class="card-head"><span class="card-title">Channel mix</span><button class="menu" aria-label="Widget options">⋯</button></header>
<div class="donut-row">
<svg class="donut" viewBox="0 0 42 42" role="img" aria-label="Channel mix: 46% organic, 28% paid, 16% social, 10% direct">
<circle class="d-track" cx="21" cy="21" r="15.9"></circle>
<circle class="d-seg s1" cx="21" cy="21" r="15.9" stroke-dasharray="46 54" stroke-dashoffset="25"></circle>
<circle class="d-seg s2" cx="21" cy="21" r="15.9" stroke-dasharray="28 72" stroke-dashoffset="79"></circle>
<circle class="d-seg s3" cx="21" cy="21" r="15.9" stroke-dasharray="16 84" stroke-dashoffset="51"></circle>
<circle class="d-seg s4" cx="21" cy="21" r="15.9" stroke-dasharray="10 90" stroke-dashoffset="35"></circle>
<text class="d-center" x="21" y="20">46%</text>
<text class="d-sub" x="21" y="25">organic</text>
</svg>
<ul class="donut-legend">
<li><i class="sw s1"></i>Organic<b>46%</b></li>
<li><i class="sw s2"></i>Paid<b>28%</b></li>
<li><i class="sw s3"></i>Social<b>16%</b></li>
<li><i class="sw s4"></i>Direct<b>10%</b></li>
</ul>
</div>
</article>
</div>
</section>
<!-- ───── Panel: Traffic (lazy) ───── -->
<section role="tabpanel" id="panel-traffic" aria-labelledby="tab-traffic" class="panel" tabindex="0" hidden></section>
<!-- ───── Panel: Revenue (lazy) ───── -->
<section role="tabpanel" id="panel-revenue" aria-labelledby="tab-revenue" class="panel" tabindex="0" hidden></section>
<!-- ───── Panel: Users (lazy) ───── -->
<section role="tabpanel" id="panel-users" aria-labelledby="tab-users" class="panel" tabindex="0" hidden></section>
</main>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Tabbed / sectioned
A product-style analytics dashboard for the fictional Northwind workspace. A fixed sidebar and a persistent top header (title, date-range segmented control, live toggle, and avatar) stay put while a tab bar — Overview, Traffic, Revenue, Users — swaps the body below. Each section carries its own KPI cards with deltas and sparklines plus larger widgets: an inline SVG/CSS bar chart of revenue versus target, a CSS donut for channel mix, a line chart, ranked horizontal bars, and a status table. All charts are hand-rolled SVG and CSS — no chart libraries, no images, no canvas.
Tabs follow the WAI-ARIA tabs pattern: role="tablist"/tab/tabpanel, aria-selected, roving tabindex, and Left/Right/Up/Down/Home/End keyboard navigation. Selecting a tab fades its panel in and slides an underline to match. The first three sections are empty in markup and are built on demand — a shimmer skeleton shows for a beat, then the real widgets render and animate in, so the page stays light until you actually open a section. The last opened tab is saved to localStorage and restored on reload.
Interactions are wired and functional: the date-range control rescales every KPI value, the live toggle ticks active-panel metrics with small realistic drifts, KPI cards are draggable to rearrange within their grid, the widget ⋯ menus and range changes raise a toast, and the sidebar collapses to an off-canvas drawer under ~720px. The responsive 12-column grid folds to two columns and then one as the viewport narrows, and a prefers-reduced-motion block disables the animations.