Dashboard — Bento-grid layout
A bento-style analytics dashboard built on a CSS grid of mixed-size tiles — a 2x2 hero area chart, four KPI cards with deltas and inline sparklines, a donut breakdown of traffic sources, a horizontal bar ranking, a live activity feed, and a goal gauge. A sticky topbar carries the brand, search and account. The refresh button re-rolls all fictional metrics with a smooth count-up animation, tiles lift on hover, headers drag to rearrange, and the grid reflows from four columns to one as the viewport narrows.
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);
--sh-3: 0 18px 44px rgba(16, 19, 34, 0.16);
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, p, ul {
margin: 0;
}
ul {
list-style: none;
padding: 0;
}
button {
font-family: inherit;
cursor: pointer;
}
:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
border-radius: 6px;
}
.skip-link {
position: absolute;
left: -999px;
top: 8px;
z-index: 100;
background: var(--white);
color: var(--brand-d);
padding: 8px 14px;
border-radius: var(--r-sm);
box-shadow: var(--sh-2);
font-weight: 600;
text-decoration: none;
}
.skip-link:focus {
left: 12px;
}
/* ============ TOPBAR ============ */
.topbar {
position: sticky;
top: 0;
z-index: 30;
display: flex;
align-items: center;
gap: 16px;
padding: 12px 24px;
background: rgba(255, 255, 255, 0.86);
backdrop-filter: saturate(140%) blur(10px);
border-bottom: 1px solid var(--line);
}
.brand {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.brand-mark {
display: grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: 10px;
color: var(--white);
background: linear-gradient(135deg, var(--brand), var(--brand-700));
box-shadow: var(--sh-1);
}
.brand-name {
font-weight: 800;
font-size: 16px;
letter-spacing: -0.02em;
}
.brand-name-soft {
color: var(--muted);
font-weight: 600;
}
.brand-tag {
font-size: 11px;
font-weight: 600;
color: var(--brand-d);
background: var(--brand-50);
padding: 3px 8px;
border-radius: 999px;
}
.topbar-search {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
max-width: 420px;
margin: 0 auto;
padding: 8px 12px;
border: 1px solid var(--line);
border-radius: 999px;
background: var(--bg);
color: var(--muted);
transition: border-color 0.15s, background 0.15s;
}
.topbar-search:focus-within {
border-color: var(--brand);
background: var(--white);
}
.topbar-search input {
flex: 1;
border: 0;
background: transparent;
font: inherit;
color: var(--ink);
min-width: 0;
}
.topbar-search input:focus {
outline: none;
}
.topbar-search kbd {
font: 600 11px/1 "Inter", sans-serif;
color: var(--muted);
background: var(--white);
border: 1px solid var(--line);
border-radius: 6px;
padding: 3px 6px;
}
.topbar-actions {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.btn {
display: inline-flex;
align-items: center;
gap: 7px;
border: 1px solid var(--line);
background: var(--white);
color: var(--ink-2);
font-weight: 600;
font-size: 13px;
padding: 8px 14px;
border-radius: 10px;
box-shadow: var(--sh-1);
transition: transform 0.12s, box-shadow 0.12s, border-color 0.12s, color 0.12s;
}
.btn:hover {
border-color: var(--line-2);
color: var(--ink);
transform: translateY(-1px);
}
.btn:active {
transform: translateY(0);
}
.btn.ghost {
background: var(--brand-50);
border-color: transparent;
color: var(--brand-d);
}
.btn.is-loading .ref-ico {
animation: spin 0.7s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.icon-btn {
position: relative;
display: grid;
place-items: center;
width: 38px;
height: 38px;
border: 1px solid var(--line);
background: var(--white);
color: var(--ink-2);
border-radius: 10px;
box-shadow: var(--sh-1);
transition: color 0.12s, border-color 0.12s, transform 0.12s;
}
.icon-btn:hover {
color: var(--ink);
transform: translateY(-1px);
}
.icon-btn .dot {
position: absolute;
top: 8px;
right: 9px;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--danger);
border: 2px solid var(--white);
}
.user {
display: flex;
align-items: center;
gap: 9px;
border: 1px solid var(--line);
background: var(--white);
padding: 5px 12px 5px 6px;
border-radius: 999px;
box-shadow: var(--sh-1);
transition: border-color 0.12s, transform 0.12s;
}
.user:hover {
border-color: var(--line-2);
transform: translateY(-1px);
}
.avatar {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 50%;
background: linear-gradient(135deg, var(--accent), #079b8f);
color: var(--white);
font-weight: 700;
font-size: 12px;
}
.user-meta {
display: flex;
flex-direction: column;
line-height: 1.15;
text-align: left;
}
.user-name {
font-weight: 700;
font-size: 13px;
}
.user-role {
font-size: 11px;
color: var(--muted);
}
/* ============ SHELL / PAGE HEAD ============ */
.shell {
max-width: 1240px;
margin: 0 auto;
padding: 24px 24px 48px;
}
.page-head {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 6px;
}
.page-head h1 {
font-size: 24px;
font-weight: 800;
letter-spacing: -0.025em;
}
.page-sub {
color: var(--muted);
font-size: 13px;
margin-top: 2px;
}
.page-sub strong {
color: var(--ink-2);
font-weight: 700;
}
.page-tools {
display: flex;
align-items: center;
gap: 14px;
}
.seg {
display: inline-flex;
padding: 3px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: 999px;
}
.seg-btn {
border: 0;
background: transparent;
color: var(--muted);
font-weight: 600;
font-size: 12.5px;
padding: 6px 13px;
border-radius: 999px;
transition: color 0.12s, background 0.12s, box-shadow 0.12s;
}
.seg-btn:hover {
color: var(--ink-2);
}
.seg-btn.is-active {
background: var(--white);
color: var(--brand-d);
box-shadow: var(--sh-1);
}
.seg.seg-sm {
background: var(--bg);
}
.seg.seg-sm .seg-btn {
padding: 5px 11px;
font-size: 12px;
}
.updated {
font-size: 12px;
color: var(--muted);
white-space: nowrap;
}
.grid-hint {
font-size: 12.5px;
color: var(--muted);
margin: 14px 0 16px;
}
.grid-hint::before {
content: "⠿ ";
color: var(--brand);
}
/* ============ BENTO GRID ============ */
.bento {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 168px;
gap: 16px;
}
.tile {
display: flex;
flex-direction: column;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-1);
overflow: hidden;
transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
}
.tile:hover {
transform: translateY(-3px);
box-shadow: var(--sh-2);
border-color: var(--line-2);
}
.tile.dragging {
opacity: 0.4;
}
.tile.drop-target {
outline: 2px dashed var(--brand);
outline-offset: -3px;
}
/* tile spans */
.tile-hero {
grid-column: span 2;
grid-row: span 2;
}
.tile-donut {
grid-row: span 2;
}
.tile-activity {
grid-row: span 2;
}
.tile-bars {
grid-column: span 2;
}
.tile-head {
display: flex;
align-items: center;
gap: 10px;
padding: 13px 14px 11px;
border-bottom: 1px solid var(--line);
flex-shrink: 0;
}
.tile-head h2 {
font-size: 13.5px;
font-weight: 700;
color: var(--ink);
letter-spacing: -0.01em;
}
.th-text {
min-width: 0;
}
.th-sub {
font-size: 11.5px;
color: var(--muted);
font-weight: 500;
margin-top: 1px;
}
.th-right {
margin-left: auto;
display: flex;
align-items: baseline;
gap: 10px;
}
.big-value {
font-size: 22px;
font-weight: 800;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
}
.drag {
color: var(--line-2);
cursor: grab;
font-size: 15px;
line-height: 1;
user-select: none;
flex-shrink: 0;
transition: color 0.12s;
}
.tile-head[draggable="true"] {
cursor: grab;
}
.tile-head[draggable="true"]:hover .drag {
color: var(--muted);
}
.tile-head:active {
cursor: grabbing;
}
.menu-btn {
margin-left: auto;
border: 0;
background: transparent;
color: var(--muted);
font-size: 18px;
line-height: 1;
width: 28px;
height: 28px;
border-radius: 8px;
flex-shrink: 0;
transition: background 0.12s, color 0.12s;
}
.th-right + .menu-btn,
.seg + .menu-btn {
margin-left: 8px;
}
.menu-btn:hover {
background: var(--bg);
color: var(--ink);
}
.tile-body {
flex: 1;
padding: 14px;
min-height: 0;
display: flex;
flex-direction: column;
}
.delta {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 12px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.delta.up { color: var(--ok); }
.delta.down { color: var(--danger); }
/* ---- HERO area chart ---- */
.area-chart {
width: 100%;
flex: 1;
min-height: 120px;
display: block;
}
.hero-legend {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--line);
}
.hero-legend li {
display: flex;
flex-direction: column;
gap: 2px;
}
.lg-k {
font-size: 11px;
color: var(--muted);
font-weight: 500;
}
.lg-v {
font-size: 15px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
/* ---- KPI tiles ---- */
.kpi-body {
justify-content: space-between;
}
.kpi-value {
font-size: 26px;
font-weight: 800;
letter-spacing: -0.025em;
font-variant-numeric: tabular-nums;
}
.kpi-foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.spark {
width: 96px;
height: 30px;
display: block;
}
/* ---- Donut tile ---- */
.donut-body {
flex-direction: column;
align-items: center;
gap: 14px;
justify-content: center;
}
.donut-wrap {
position: relative;
width: 132px;
height: 132px;
}
.donut {
width: 132px;
height: 132px;
}
.donut .seg {
transition: stroke-dashoffset 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.donut-center {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
pointer-events: none;
}
.dc-num {
font-size: 22px;
font-weight: 800;
letter-spacing: -0.02em;
}
.dc-lbl {
font-size: 11px;
color: var(--muted);
}
.donut-legend {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 7px 14px;
width: 100%;
}
.donut-legend li {
display: flex;
align-items: center;
gap: 7px;
font-size: 12.5px;
color: var(--ink-2);
font-weight: 500;
}
.ld-dot {
width: 9px;
height: 9px;
border-radius: 3px;
flex-shrink: 0;
}
.ld-v {
margin-left: auto;
font-weight: 700;
color: var(--ink);
font-variant-numeric: tabular-nums;
}
/* ---- Bars tile ---- */
.bars {
display: flex;
flex-direction: column;
gap: 11px;
width: 100%;
}
.bars li {
display: grid;
grid-template-columns: 110px 1fr 64px;
align-items: center;
gap: 12px;
}
.bar-k {
font-size: 12.5px;
font-weight: 600;
color: var(--ink-2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bar-track {
height: 9px;
border-radius: 999px;
background: var(--bg);
overflow: hidden;
}
.bar-fill {
display: block;
height: 100%;
width: var(--bw, 0%);
border-radius: 999px;
background: linear-gradient(90deg, var(--brand), var(--accent));
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.bar-v {
font-size: 12.5px;
font-weight: 700;
text-align: right;
font-variant-numeric: tabular-nums;
}
/* ---- Activity feed ---- */
.live-pill {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
font-weight: 700;
color: var(--ok);
background: rgba(47, 158, 111, 0.12);
padding: 3px 9px;
border-radius: 999px;
margin-left: auto;
}
.live-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--ok);
animation: pulse 1.6s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.45; transform: scale(0.7); }
}
.live-pill + .menu-btn {
margin-left: 8px;
}
.feed {
display: flex;
flex-direction: column;
gap: 2px;
overflow-y: auto;
width: 100%;
}
.feed-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 9px 4px;
border-bottom: 1px solid var(--line);
}
.feed-item:last-child {
border-bottom: 0;
}
.feed-item.is-new {
animation: slideIn 0.4s ease;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-6px); }
to { opacity: 1; transform: translateY(0); }
}
.fi-ico {
display: grid;
place-items: center;
width: 26px;
height: 26px;
border-radius: 8px;
font-size: 13px;
font-weight: 700;
flex-shrink: 0;
}
.fi-ico.ok { background: rgba(47, 158, 111, 0.14); color: var(--ok); }
.fi-ico.brand { background: var(--brand-50); color: var(--brand-d); }
.fi-ico.warn { background: rgba(217, 138, 43, 0.14); color: var(--warn); }
.fi-text {
font-size: 12.5px;
color: var(--muted);
line-height: 1.4;
}
.fi-text strong {
color: var(--ink);
font-weight: 700;
}
.fi-time {
display: block;
font-size: 11px;
color: var(--muted);
margin-top: 1px;
}
/* ---- Goal gauge ---- */
.goal-body {
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
}
.gauge {
width: 100%;
max-width: 180px;
height: auto;
}
#gaugeArc {
transition: stroke-dasharray 0.7s cubic-bezier(0.4, 0, 0.2, 1);
}
.goal-meta {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.goal-pct {
font-size: 24px;
font-weight: 800;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
}
.goal-sub {
font-size: 12px;
color: var(--muted);
}
.goal-sub [data-goalcur] {
color: var(--ink-2);
font-weight: 700;
}
/* ============ TOAST ============ */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 24px);
background: var(--ink);
color: var(--white);
font-size: 13px;
font-weight: 600;
padding: 11px 18px;
border-radius: 12px;
box-shadow: var(--sh-3);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s, transform 0.25s;
z-index: 60;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ============ RESPONSIVE ============ */
@media (max-width: 1080px) {
.bento {
grid-template-columns: repeat(2, 1fr);
}
.tile-hero { grid-column: span 2; }
.tile-bars { grid-column: span 2; }
}
@media (max-width: 720px) {
.topbar {
flex-wrap: wrap;
padding: 10px 16px;
}
.topbar-search {
order: 3;
max-width: none;
width: 100%;
margin: 4px 0 0;
}
.topbar-actions {
margin-left: auto;
}
.btn span,
.user-meta {
display: none;
}
.btn {
padding: 8px 10px;
}
.user {
padding: 5px;
}
.shell {
padding: 18px 16px 40px;
}
.page-tools {
width: 100%;
justify-content: space-between;
}
.bento {
grid-template-columns: 1fr;
grid-auto-rows: auto;
}
.tile,
.tile-hero,
.tile-donut,
.tile-activity,
.tile-bars {
grid-column: auto;
grid-row: auto;
}
.tile-hero .area-chart { min-height: 150px; }
.feed { max-height: 280px; }
}
@media (max-width: 380px) {
.page-head h1 { font-size: 21px; }
.bars li {
grid-template-columns: 84px 1fr 56px;
gap: 8px;
}
.hero-legend {
grid-template-columns: repeat(2, 1fr);
}
.donut-legend {
grid-template-columns: 1fr;
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}(function () {
"use strict";
/* ---------- tiny helpers ---------- */
var $ = function (s, r) { return (r || document).querySelector(s); };
var $$ = function (s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); };
var rand = function (min, max) { return Math.random() * (max - min) + min; };
var clamp = function (v, a, b) { return Math.max(a, Math.min(b, v)); };
/* ---------- toast ---------- */
var toastEl = $("#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(node, value) {
var prefix = node.getAttribute("data-prefix") || "";
var suffix = node.getAttribute("data-suffix") || "";
var decimals = parseInt(node.getAttribute("data-decimals") || "0", 10);
var scale = parseFloat(node.getAttribute("data-scale") || "1");
var scaled = value / scale;
var str;
if (decimals > 0) {
str = scaled.toFixed(decimals);
} else {
str = Math.round(scaled).toLocaleString("en-US");
}
return prefix + str + suffix;
}
/* ---------- count-up animation ---------- */
function animateCount(node, to, dur) {
dur = dur || 750;
var from = parseFloat(node.getAttribute("data-current") || node.getAttribute("data-target") || "0");
node.setAttribute("data-target", to);
var start = performance.now();
function step(now) {
var t = clamp((now - start) / dur, 0, 1);
var eased = 1 - Math.pow(1 - t, 3);
var val = from + (to - from) * eased;
node.setAttribute("data-current", val);
node.textContent = fmt(node, val);
if (t < 1) requestAnimationFrame(step);
else { node.setAttribute("data-current", to); node.textContent = fmt(node, to); }
}
requestAnimationFrame(step);
}
/* ---------- demo dataset (re-rollable) ---------- */
var state = {};
function roll() {
var n = 28;
state.series = [];
var base = rand(11000, 15000);
for (var i = 0; i < n; i++) {
base += rand(-1800, 2300);
base = clamp(base, 6000, 32000);
state.series.push(Math.round(base));
}
state.revenue = state.series.reduce(function (a, b) { return a + b; }, 0);
state.best = Math.max.apply(null, state.series);
state.avg = Math.round(state.revenue / n);
state.orders = Math.round(state.revenue / rand(140, 168));
state.aov = state.revenue / state.orders;
state.users = Math.round(rand(68000, 82000));
state.conv = Math.round(rand(310, 430)); // /100 => %
state.ret = Math.round(rand(880, 945)); // /10 => %
state.mrr = Math.round(rand(118000, 142000));
state.deltas = {
hero: rand(-4, 16),
users: rand(-3, 12),
conv: rand(-5, 7),
ret: rand(-2, 6),
mrr: rand(-2, 9)
};
state.sparks = {};
["users", "conv", "ret", "mrr"].forEach(function (k) {
var arr = [], v = rand(30, 70);
for (var i = 0; i < 16; i++) { v += rand(-12, 14); v = clamp(v, 8, 92); arr.push(v); }
state.sparks[k] = arr;
});
// donut shares (sum 100)
var raw = [rand(34, 46), rand(22, 32), rand(14, 22), rand(8, 14)];
var sum = raw.reduce(function (a, b) { return a + b; }, 0);
state.shares = raw.map(function (x) { return Math.round((x / sum) * 100); });
// fix rounding to exactly 100
var diff = 100 - state.shares.reduce(function (a, b) { return a + b; }, 0);
state.shares[0] += diff;
// bars
var labels = ["Marketplace", "Direct store", "Wholesale", "Subscriptions", "Affiliates"];
state.bars = { rev: [], ord: [] };
labels.forEach(function (l) {
state.bars.rev.push({ k: l, v: Math.round(rand(40, 160) * 1000) });
state.bars.ord.push({ k: l, v: Math.round(rand(280, 1400)) });
});
state.bars.rev.sort(function (a, b) { return b.v - a.v; });
state.bars.ord.sort(function (a, b) { return b.v - a.v; });
state.goalTarget = 625000;
state.goalCur = state.revenue;
}
/* ---------- SVG path builders ---------- */
function buildLine(series, w, h, pad) {
pad = pad || 0;
var max = Math.max.apply(null, series);
var min = Math.min.apply(null, series);
var span = (max - min) || 1;
var inner = h - pad * 2;
var pts = series.map(function (v, i) {
var x = (i / (series.length - 1)) * w;
var y = pad + inner - ((v - min) / span) * inner;
return [x, y];
});
var d = "M" + pts[0][0].toFixed(1) + " " + pts[0][1].toFixed(1);
for (var i = 1; i < pts.length; i++) {
var p0 = pts[i - 1], p1 = pts[i];
var cx = (p0[0] + p1[0]) / 2;
d += " C" + cx.toFixed(1) + " " + p0[1].toFixed(1) + " " + cx.toFixed(1) + " " + p1[1].toFixed(1) + " " + p1[0].toFixed(1) + " " + p1[1].toFixed(1);
}
return { d: d, pts: pts };
}
/* ---------- renderers ---------- */
function renderHero() {
var W = 640, H = 220, PAD = 16;
var built = buildLine(state.series, W, H, PAD);
var line = $("#linePath");
var area = $("#areaPath");
var dot = $("#lineDot");
if (line) line.setAttribute("d", built.d);
if (area) area.setAttribute("d", built.d + " L" + W + " " + H + " L0 " + H + " Z");
if (dot) {
var last = built.pts[built.pts.length - 1];
dot.setAttribute("cx", last[0]);
dot.setAttribute("cy", last[1]);
}
var hv = $('[data-tile="hero"] .big-value');
if (hv) animateCount(hv, state.revenue);
setDelta($('[data-tile="hero"] .delta'), state.deltas.hero);
animateCount($("[data-best]"), state.best);
animateCount($("[data-avg]"), state.avg);
animateCount($("[data-orders]"), state.orders);
var aov = $("[data-aov]");
if (aov) aov.textContent = "$" + state.aov.toFixed(2);
}
function setDelta(node, val) {
if (!node) return;
var up = val >= 0;
node.classList.toggle("up", up);
node.classList.toggle("down", !up);
node.setAttribute("aria-label", (up ? "up " : "down ") + Math.abs(val).toFixed(1) + " percent");
node.innerHTML =
'<svg viewBox="0 0 12 12" width="11" height="11" aria-hidden="true"><path d="' +
(up ? "M6 2 10 8H2Z" : "M6 10 2 4h8Z") +
'" fill="currentColor"/></svg>' + Math.abs(val).toFixed(1) + "%";
}
function renderKpi(tile, target, deltaVal, sparkKey, sparkColor) {
var el = $('[data-tile="' + tile + '"]');
if (!el) return;
animateCount($(".kpi-value", el), target);
setDelta($(".delta", el), deltaVal);
var spark = $("[data-spark]", el);
if (spark) spark.setAttribute("d", buildLine(state.sparks[sparkKey], 120, 36, 4).d);
}
function renderKpis() {
renderKpi("kpi-users", state.users, state.deltas.users, "users");
renderKpi("kpi-conv", state.conv, state.deltas.conv, "conv");
renderKpi("kpi-ret", state.ret, state.deltas.ret, "ret");
renderKpi("kpi-mrr", state.mrr, state.deltas.mrr, "mrr");
}
function renderDonut() {
var C = 2 * Math.PI * 46; // circumference
var segs = $$(".donut .seg");
var offset = 0;
state.shares.forEach(function (pct, i) {
var seg = segs[i];
if (!seg) return;
var len = (pct / 100) * C;
seg.setAttribute("stroke-dasharray", len.toFixed(2) + " " + (C - len).toFixed(2));
seg.setAttribute("stroke-dashoffset", (-offset).toFixed(2));
offset += len;
var lbl = $('[data-share="' + i + '"]');
if (lbl) lbl.textContent = pct + "%";
});
}
function renderBars(metric) {
metric = metric || currentBarMetric;
var data = state.bars[metric];
var max = Math.max.apply(null, data.map(function (d) { return d.v; }));
var list = $("#barList");
if (!list) return;
list.innerHTML = data.map(function (d) {
var pct = Math.round((d.v / max) * 100);
var v = metric === "rev"
? "$" + (d.v / 1000).toFixed(1) + "k"
: d.v.toLocaleString("en-US");
return '<li><span class="bar-k">' + d.k + '</span>' +
'<span class="bar-track"><span class="bar-fill" style="--bw:0%"></span></span>' +
'<span class="bar-v">' + v + '</span></li>';
}).join("");
// animate widths in next frame
requestAnimationFrame(function () {
$$(".bar-fill", list).forEach(function (f, i) {
var pct = Math.round((data[i].v / max) * 100);
f.style.setProperty("--bw", pct + "%");
});
});
}
function renderGoal() {
var pct = clamp(Math.round((state.goalCur / state.goalTarget) * 100), 0, 100);
var arc = $("#gaugeArc");
if (arc) {
var L = arc.getTotalLength ? arc.getTotalLength() : 151;
arc.style.strokeDasharray = (L * pct / 100).toFixed(1) + " " + L.toFixed(1);
}
var pctEl = $(".goal-pct");
if (pctEl) animateCount(pctEl, pct * 10); // scale 10 => "78%"
var cur = $("[data-goalcur]");
if (cur) cur.textContent = "$" + (state.goalCur / 1000).toFixed(1) + "k";
}
/* ---------- range label ---------- */
function setRangeLabel(days) {
var label = days >= 365 ? "12 months" : days + " days";
$$("[data-rangelabel]").forEach(function (n) { n.textContent = label; });
}
/* ---------- full render ---------- */
function renderAll() {
renderHero();
renderKpis();
renderDonut();
renderBars();
renderGoal();
stampUpdated();
}
function stampUpdated() {
var u = $("#updatedAt");
if (u) u.textContent = "updated just now";
}
/* ---------- range segmented control ---------- */
$$("#rangeSeg .seg-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
$$("#rangeSeg .seg-btn").forEach(function (b) {
b.classList.remove("is-active");
b.setAttribute("aria-selected", "false");
});
btn.classList.add("is-active");
btn.setAttribute("aria-selected", "true");
var days = parseInt(btn.getAttribute("data-range"), 10);
setRangeLabel(days);
// longer ranges scale the numbers up
var mult = days / 30;
roll();
state.revenue = Math.round(state.revenue * (0.6 + mult * 0.4));
state.avg = Math.round(state.revenue / 28);
renderAll();
toast("Range set to " + (days >= 365 ? "12 months" : days + " days"));
});
});
/* ---------- bar tabs ---------- */
var currentBarMetric = "rev";
$$("#barTabs .seg-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
$$("#barTabs .seg-btn").forEach(function (b) {
b.classList.remove("is-active");
b.setAttribute("aria-selected", "false");
});
btn.classList.add("is-active");
btn.setAttribute("aria-selected", "true");
currentBarMetric = btn.getAttribute("data-metric");
renderBars(currentBarMetric);
});
});
/* ---------- refresh ---------- */
var refreshBtn = $("#refreshBtn");
if (refreshBtn) {
refreshBtn.addEventListener("click", function () {
refreshBtn.classList.add("is-loading");
refreshBtn.disabled = true;
roll();
renderAll();
toast("Demo data refreshed");
setTimeout(function () {
refreshBtn.classList.remove("is-loading");
refreshBtn.disabled = false;
}, 700);
});
}
/* ---------- tile menus ---------- */
$$(".menu-btn").forEach(function (b) {
b.addEventListener("click", function (e) {
e.stopPropagation();
var tile = b.closest(".tile");
var name = tile ? $("h2", tile).textContent : "Tile";
toast(name + ": menu (demo)");
});
});
/* ---------- search ---------- */
var search = $("#globalSearch");
if (search) {
search.addEventListener("keydown", function (e) {
if (e.key === "Enter" && search.value.trim()) {
toast('Searching "' + search.value.trim() + '"…');
}
});
}
document.addEventListener("keydown", function (e) {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
e.preventDefault();
if (search) search.focus();
}
});
/* ---------- live activity feed ---------- */
var feed = $("#feed");
var feedPool = [
{ ico: "ok", cls: "ok", t: "<strong>New order</strong> · #_ID placed" },
{ ico: "✦", cls: "brand", t: "<strong>Plan upgrade</strong> · _CO → Scale" },
{ ico: "+", cls: "brand", t: "<strong>Signup</strong> · _USER joined" },
{ ico: "!", cls: "warn", t: "<strong>Refund</strong> · #_ID reversed" },
{ ico: "$", cls: "ok", t: "<strong>Payment</strong> · $_AMT received" }
];
var names = ["Aria K.", "Marlow V.", "Jin P.", "Sofia R.", "Devon T.", "Noor A."];
var cos = ["Helix Co", "Vire", "Pallas", "Orbit Labs", "Kestrel"];
function liveTick() {
if (!feed) return;
var p = feedPool[Math.floor(rand(0, feedPool.length))];
var txt = p.t
.replace("_ID", Math.floor(rand(40200, 40999)))
.replace("_CO", cos[Math.floor(rand(0, cos.length))])
.replace("_USER", names[Math.floor(rand(0, names.length))])
.replace("_AMT", Math.floor(rand(40, 980)).toLocaleString("en-US"));
var li = document.createElement("li");
li.className = "feed-item is-new";
var icoChar = p.ico.length === 1 && /[a-z]/i.test(p.ico) === false ? p.ico : p.ico;
li.innerHTML =
'<span class="fi-ico ' + p.cls + '" aria-hidden="true">' + p.ico + '</span>' +
'<div class="fi-text">' + txt + '<span class="fi-time">just now</span></div>';
feed.insertBefore(li, feed.firstChild);
// age existing timestamps
var times = $$(".fi-time", feed);
for (var i = 1; i < times.length; i++) {
if (times[i].textContent === "just now") times[i].textContent = "moments ago";
}
while (feed.children.length > 7) feed.removeChild(feed.lastChild);
}
var liveTimer = setInterval(liveTick, 5200);
/* ---------- drag to rearrange ---------- */
var bento = $("#bento");
var dragSrc = null;
$$(".tile-head[draggable='true']").forEach(function (head) {
var tile = head.closest(".tile");
head.addEventListener("dragstart", function (e) {
dragSrc = tile;
tile.classList.add("dragging");
e.dataTransfer.effectAllowed = "move";
try { e.dataTransfer.setData("text/plain", tile.dataset.tile || ""); } catch (err) {}
});
head.addEventListener("dragend", function () {
tile.classList.remove("dragging");
$$(".tile").forEach(function (t) { t.classList.remove("drop-target"); });
dragSrc = null;
});
});
if (bento) {
$$(".tile").forEach(function (tile) {
tile.addEventListener("dragover", function (e) {
if (!dragSrc || dragSrc === tile) return;
e.preventDefault();
e.dataTransfer.dropEffect = "move";
tile.classList.add("drop-target");
});
tile.addEventListener("dragleave", function () {
tile.classList.remove("drop-target");
});
tile.addEventListener("drop", function (e) {
e.preventDefault();
tile.classList.remove("drop-target");
if (!dragSrc || dragSrc === tile) return;
// swap DOM positions
var sibling = dragSrc.nextSibling === tile ? dragSrc : tile.nextSibling;
bento.insertBefore(dragSrc, tile);
if (sibling) bento.insertBefore(tile, sibling);
toast("Tiles rearranged");
});
});
}
/* ---------- init ---------- */
roll();
setRangeLabel(30);
// small delay so transitions are visible on first paint
renderAll();
// expose for debugging / demos
window.__bento = { roll: roll, render: renderAll, toast: toast };
// clean up interval if page hidden long-term (best effort)
window.addEventListener("beforeunload", function () { clearInterval(liveTimer); });
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Nimbus Analytics — Bento 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>
<a class="skip-link" href="#main">Skip to content</a>
<header class="topbar" role="banner">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none">
<path d="M4 14a5 5 0 0 1 1.6-9.7A6 6 0 0 1 17.5 5 4.5 4.5 0 0 1 18 14H4Z" fill="currentColor" opacity=".9"/>
<circle cx="16" cy="18" r="2.4" fill="#fff" opacity=".85"/>
</svg>
</span>
<span class="brand-name">Nimbus<span class="brand-name-soft">Analytics</span></span>
<span class="brand-tag">workspace</span>
</div>
<div class="topbar-search" role="search">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><circle cx="11" cy="11" r="7" stroke="currentColor" stroke-width="2" fill="none"/><path d="m20 20-3.2-3.2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<input type="search" id="globalSearch" placeholder="Search metrics, reports…" aria-label="Search" />
<kbd>⌘K</kbd>
</div>
<div class="topbar-actions">
<button class="btn ghost" id="refreshBtn" type="button" aria-label="Refresh data">
<svg class="ref-ico" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M20 11a8 8 0 1 0-.7 3.6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/><path d="M20 5v5h-5" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span>Refresh</span>
</button>
<button class="icon-btn" type="button" aria-label="Notifications">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M6 9a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6Z" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linejoin="round"/><path d="M10 19a2 2 0 0 0 4 0" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round"/></svg>
<span class="dot" aria-hidden="true"></span>
</button>
<button class="user" type="button" aria-label="Account: Dana Okafor">
<span class="avatar" aria-hidden="true">DO</span>
<span class="user-meta">
<span class="user-name">Dana Okafor</span>
<span class="user-role">Growth Lead</span>
</span>
</button>
</div>
</header>
<main id="main" class="shell" role="main">
<div class="page-head">
<div>
<h1>Overview</h1>
<p class="page-sub">Performance snapshot for <strong>Nimbus Analytics</strong> · all channels</p>
</div>
<div class="page-tools" role="group" aria-label="Date range">
<div class="seg" id="rangeSeg" role="tablist" aria-label="Select range">
<button class="seg-btn" role="tab" data-range="7" aria-selected="false">7d</button>
<button class="seg-btn is-active" role="tab" data-range="30" aria-selected="true">30d</button>
<button class="seg-btn" role="tab" data-range="90" aria-selected="false">90d</button>
<button class="seg-btn" role="tab" data-range="365" aria-selected="false">12m</button>
</div>
<span class="updated" id="updatedAt">updated just now</span>
</div>
</div>
<p class="grid-hint">Tip: drag a tile by its header (⠿) to rearrange the board.</p>
<section class="bento" id="bento" aria-label="Dashboard tiles">
<!-- HERO: spans 2x2 -->
<article class="tile tile-hero" data-tile="hero">
<header class="tile-head" draggable="true">
<span class="drag" aria-hidden="true">⠿</span>
<div class="th-text">
<h2>Net revenue</h2>
<p class="th-sub">Daily, last <span data-rangelabel>30 days</span></p>
</div>
<div class="th-right">
<span class="big-value" data-count data-target="486200" data-prefix="$">$486,200</span>
<span class="delta up" aria-label="up 12.4 percent"><svg viewBox="0 0 12 12" width="11" height="11" aria-hidden="true"><path d="M6 2 10 8H2Z" fill="currentColor"/></svg>12.4%</span>
</div>
<button class="menu-btn" type="button" aria-label="Tile menu">⋯</button>
</header>
<div class="tile-body">
<svg class="area-chart" viewBox="0 0 640 220" preserveAspectRatio="none" role="img" aria-label="Revenue trend area chart">
<defs>
<linearGradient id="areaFill" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="var(--brand)" stop-opacity="0.28"/>
<stop offset="100%" stop-color="var(--brand)" stop-opacity="0"/>
</linearGradient>
</defs>
<g class="grid-lines" stroke="var(--line)" stroke-width="1">
<line x1="0" y1="44" x2="640" y2="44"/>
<line x1="0" y1="103" x2="640" y2="103"/>
<line x1="0" y1="162" x2="640" y2="162"/>
</g>
<path id="areaPath" d="" fill="url(#areaFill)"></path>
<path id="linePath" d="" fill="none" stroke="var(--brand)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="lineDot" r="4.5" fill="var(--white)" stroke="var(--brand)" stroke-width="3"/>
</svg>
<ul class="hero-legend">
<li><span class="lg-k">Best day</span><span class="lg-v" data-best>$28,940</span></li>
<li><span class="lg-k">Avg / day</span><span class="lg-v" data-avg>$16,206</span></li>
<li><span class="lg-k">Orders</span><span class="lg-v" data-orders>3,184</span></li>
<li><span class="lg-k">AOV</span><span class="lg-v" data-aov>$152.70</span></li>
</ul>
</div>
</article>
<!-- KPI tiles -->
<article class="tile tile-kpi" data-tile="kpi-users">
<header class="tile-head" draggable="true">
<span class="drag" aria-hidden="true">⠿</span>
<h2>Active users</h2>
<button class="menu-btn" type="button" aria-label="Tile menu">⋯</button>
</header>
<div class="tile-body kpi-body">
<span class="kpi-value" data-count data-target="74820">74,820</span>
<div class="kpi-foot">
<span class="delta up"><svg viewBox="0 0 12 12" width="10" height="10" aria-hidden="true"><path d="M6 2 10 8H2Z" fill="currentColor"/></svg>8.1%</span>
<svg class="spark" viewBox="0 0 120 36" preserveAspectRatio="none" aria-hidden="true"><path data-spark fill="none" stroke="var(--brand)" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" d=""/></svg>
</div>
</div>
</article>
<article class="tile tile-kpi" data-tile="kpi-conv">
<header class="tile-head" draggable="true">
<span class="drag" aria-hidden="true">⠿</span>
<h2>Conversion</h2>
<button class="menu-btn" type="button" aria-label="Tile menu">⋯</button>
</header>
<div class="tile-body kpi-body">
<span class="kpi-value" data-count data-target="384" data-suffix="%" data-decimals="2" data-scale="100">3.84%</span>
<div class="kpi-foot">
<span class="delta down"><svg viewBox="0 0 12 12" width="10" height="10" aria-hidden="true"><path d="M6 10 2 4h8Z" fill="currentColor"/></svg>1.3%</span>
<svg class="spark" viewBox="0 0 120 36" preserveAspectRatio="none" aria-hidden="true"><path data-spark fill="none" stroke="var(--accent)" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" d=""/></svg>
</div>
</div>
</article>
<article class="tile tile-kpi" data-tile="kpi-ret">
<header class="tile-head" draggable="true">
<span class="drag" aria-hidden="true">⠿</span>
<h2>Retention</h2>
<button class="menu-btn" type="button" aria-label="Tile menu">⋯</button>
</header>
<div class="tile-body kpi-body">
<span class="kpi-value" data-count data-target="912" data-suffix="%" data-decimals="1" data-scale="10">91.2%</span>
<div class="kpi-foot">
<span class="delta up"><svg viewBox="0 0 12 12" width="10" height="10" aria-hidden="true"><path d="M6 2 10 8H2Z" fill="currentColor"/></svg>2.6%</span>
<svg class="spark" viewBox="0 0 120 36" preserveAspectRatio="none" aria-hidden="true"><path data-spark fill="none" stroke="var(--ok)" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" d=""/></svg>
</div>
</div>
</article>
<article class="tile tile-kpi" data-tile="kpi-mrr">
<header class="tile-head" draggable="true">
<span class="drag" aria-hidden="true">⠿</span>
<h2>MRR</h2>
<button class="menu-btn" type="button" aria-label="Tile menu">⋯</button>
</header>
<div class="tile-body kpi-body">
<span class="kpi-value" data-count data-target="128400" data-prefix="$">$128,400</span>
<div class="kpi-foot">
<span class="delta up"><svg viewBox="0 0 12 12" width="10" height="10" aria-hidden="true"><path d="M6 2 10 8H2Z" fill="currentColor"/></svg>5.9%</span>
<svg class="spark" viewBox="0 0 120 36" preserveAspectRatio="none" aria-hidden="true"><path data-spark fill="none" stroke="var(--warn)" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" d=""/></svg>
</div>
</div>
</article>
<!-- Donut tile -->
<article class="tile tile-donut" data-tile="donut">
<header class="tile-head" draggable="true">
<span class="drag" aria-hidden="true">⠿</span>
<h2>Traffic sources</h2>
<button class="menu-btn" type="button" aria-label="Tile menu">⋯</button>
</header>
<div class="tile-body donut-body">
<div class="donut-wrap">
<svg class="donut" viewBox="0 0 120 120" role="img" aria-label="Traffic sources donut chart">
<circle class="donut-track" cx="60" cy="60" r="46" fill="none" stroke="var(--line)" stroke-width="16"/>
<circle class="seg" data-seg="0" cx="60" cy="60" r="46" fill="none" stroke="var(--brand)" stroke-width="16" stroke-linecap="round" transform="rotate(-90 60 60)"/>
<circle class="seg" data-seg="1" cx="60" cy="60" r="46" fill="none" stroke="var(--accent)" stroke-width="16" stroke-linecap="round" transform="rotate(-90 60 60)"/>
<circle class="seg" data-seg="2" cx="60" cy="60" r="46" fill="none" stroke="var(--warn)" stroke-width="16" stroke-linecap="round" transform="rotate(-90 60 60)"/>
<circle class="seg" data-seg="3" cx="60" cy="60" r="46" fill="none" stroke="var(--ink-2)" stroke-width="16" stroke-linecap="round" transform="rotate(-90 60 60)"/>
</svg>
<div class="donut-center"><span class="dc-num" data-donuttotal>100%</span><span class="dc-lbl">total</span></div>
</div>
<ul class="donut-legend" id="donutLegend">
<li><span class="ld-dot" style="background:var(--brand)"></span>Organic<span class="ld-v" data-share="0">42%</span></li>
<li><span class="ld-dot" style="background:var(--accent)"></span>Direct<span class="ld-v" data-share="1">28%</span></li>
<li><span class="ld-dot" style="background:var(--warn)"></span>Paid<span class="ld-v" data-share="2">19%</span></li>
<li><span class="ld-dot" style="background:var(--ink-2)"></span>Referral<span class="ld-v" data-share="3">11%</span></li>
</ul>
</div>
</article>
<!-- Bar / list tile (wide) -->
<article class="tile tile-bars" data-tile="bars">
<header class="tile-head" draggable="true">
<span class="drag" aria-hidden="true">⠿</span>
<div class="th-text"><h2>Revenue by channel</h2></div>
<div class="seg seg-sm" id="barTabs" role="tablist" aria-label="Channel metric">
<button class="seg-btn is-active" role="tab" data-metric="rev" aria-selected="true">Revenue</button>
<button class="seg-btn" role="tab" data-metric="ord" aria-selected="false">Orders</button>
</div>
<button class="menu-btn" type="button" aria-label="Tile menu">⋯</button>
</header>
<div class="tile-body">
<ul class="bars" id="barList">
<li><span class="bar-k">Marketplace</span><span class="bar-track"><span class="bar-fill" style="--bw:88%"></span></span><span class="bar-v">$152.1k</span></li>
<li><span class="bar-k">Direct store</span><span class="bar-track"><span class="bar-fill" style="--bw:71%"></span></span><span class="bar-v">$122.8k</span></li>
<li><span class="bar-k">Wholesale</span><span class="bar-track"><span class="bar-fill" style="--bw:54%"></span></span><span class="bar-v">$93.4k</span></li>
<li><span class="bar-k">Subscriptions</span><span class="bar-track"><span class="bar-fill" style="--bw:41%"></span></span><span class="bar-v">$70.9k</span></li>
<li><span class="bar-k">Affiliates</span><span class="bar-track"><span class="bar-fill" style="--bw:27%"></span></span><span class="bar-v">$46.7k</span></li>
</ul>
</div>
</article>
<!-- Activity tile (tall) -->
<article class="tile tile-activity" data-tile="activity">
<header class="tile-head" draggable="true">
<span class="drag" aria-hidden="true">⠿</span>
<h2>Live activity</h2>
<span class="live-pill"><span class="live-dot" aria-hidden="true"></span>live</span>
<button class="menu-btn" type="button" aria-label="Tile menu">⋯</button>
</header>
<div class="tile-body">
<ul class="feed" id="feed">
<li class="feed-item"><span class="fi-ico ok" aria-hidden="true">$</span><div class="fi-text"><strong>New order</strong> · Aria K. placed #40291<span class="fi-time">just now</span></div></li>
<li class="feed-item"><span class="fi-ico brand" aria-hidden="true">✦</span><div class="fi-text"><strong>Plan upgrade</strong> · Helix Co → Scale<span class="fi-time">2m ago</span></div></li>
<li class="feed-item"><span class="fi-ico warn" aria-hidden="true">!</span><div class="fi-text"><strong>Refund</strong> · #40188 reversed<span class="fi-time">7m ago</span></div></li>
<li class="feed-item"><span class="fi-ico brand" aria-hidden="true">+</span><div class="fi-text"><strong>Signup</strong> · marlow@vire.io joined<span class="fi-time">11m ago</span></div></li>
</ul>
</div>
</article>
<!-- Goal tile -->
<article class="tile tile-goal" data-tile="goal">
<header class="tile-head" draggable="true">
<span class="drag" aria-hidden="true">⠿</span>
<h2>Quarterly goal</h2>
<button class="menu-btn" type="button" aria-label="Tile menu">⋯</button>
</header>
<div class="tile-body goal-body">
<svg class="gauge" viewBox="0 0 120 70" role="img" aria-label="Goal progress 78 percent">
<path d="M12 64 A48 48 0 0 1 108 64" fill="none" stroke="var(--line)" stroke-width="12" stroke-linecap="round"/>
<path id="gaugeArc" d="M12 64 A48 48 0 0 1 108 64" fill="none" stroke="var(--accent)" stroke-width="12" stroke-linecap="round"/>
</svg>
<div class="goal-meta">
<span class="goal-pct" data-count data-target="780" data-suffix="%" data-decimals="0" data-scale="10">78%</span>
<span class="goal-sub"><span data-goalcur>$486.2k</span> of <span class="goal-target">$625k</span></span>
</div>
</div>
</article>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Bento-grid layout
A self-contained product dashboard for the fictional Nimbus Analytics workspace, laid out as a
bento grid: one CSS grid of unequal tiles where a hero revenue chart spans 2×2, the donut and
activity tiles run two rows tall, and the channel-ranking tile spans two columns. Every widget is a
card with its own header, optional ⋯ menu and a drag handle. All charts are hand-built inline
SVG — a smoothed area/line for the hero, four KPI sparklines, a stroke-dasharray donut, and a
semicircular goal gauge — so there are no chart libraries, <img> tags or canvas anywhere.
Interactions are all vanilla JS. The date-range segmented control (7d / 30d / 90d / 12m) and the
Refresh button both re-roll the demo dataset and re-render every tile, with KPI and total values
counting up via a cubic ease. The bars tile toggles between Revenue and Orders, the activity feed
ticks in a new fictional event every few seconds, and tiles can be dragged by their header to swap
positions on the board. A toast() helper confirms each action.
The whole thing is keyboard-usable and AA-contrast: landmark roles on the topbar and main, aria
labels on deltas and charts, a skip link, visible focus rings, and ⌘K/Ctrl+K to jump to search.
The grid collapses to two columns by ~1080px and a single column by ~720px, stays usable down to
~360px, and respects prefers-reduced-motion.