Dashboards
Chart card with legend
CSS-only stacked bars and range picker
CSS-only stacked bars and range picker
Build an analytics chart card with Tailwind CSS using no charting library. The card header has a title, a subtitle with the total and a green delta, and a small segmented range picker (7d / 30d / 90d) where one option is active. The chart body is a flex row of column groups; each column is a rounded stacked bar built from two divs of different heights and shades, aligned to the bottom with items-end. Add x-axis labels beneath and a legend row with two colored dots naming the series. Then add JavaScript that renders the bars from a data array instead of fixed classes: hold one dataset per range, scale every bar as a percentage of the largest total so the chart always fills its height, redraw the bars, labels, total, and delta when the range changes, move the active pill, and give each column a title tooltip with its exact figures. <div class="mx-auto max-w-3xl p-6">
<div class="rounded-2xl border border-neutral-200 bg-white p-6">
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<h2 class="font-semibold text-neutral-900">Revenue by month</h2>
<p class="mt-1 text-sm text-neutral-500"><span id="chartTotal">$412,900</span> <span id="chartDelta" class="ml-1 rounded bg-green-100 px-1.5 py-0.5 text-xs font-medium text-green-700">↑ 9.4%</span></p>
</div>
<div id="ranges" class="flex gap-0.5 rounded-lg bg-neutral-100 p-0.5 text-xs">
<button data-range="7d" class="rounded-md px-2.5 py-1 text-neutral-500">7d</button>
<button data-range="30d" class="rounded-md bg-white px-2.5 py-1 font-medium text-neutral-900 shadow-sm">30d</button>
<button data-range="90d" class="rounded-md px-2.5 py-1 text-neutral-500">90d</button>
</div>
</div>
<div id="chart" class="mt-8 flex h-44 items-stretch gap-2"></div>
<div id="chartLabels" class="mt-2 flex gap-2 text-[11px] text-neutral-400"></div>
<div class="mt-6 flex gap-6 border-t border-neutral-100 pt-4 text-xs text-neutral-500">
<span class="flex items-center gap-2"><span class="h-2 w-2 rounded-full bg-neutral-900"></span> New business</span>
<span class="flex items-center gap-2"><span class="h-2 w-2 rounded-full bg-neutral-300"></span> Expansion</span>
</div>
</div>
</div> // [label, new business, expansion] — swap for your own series.
var DATA = {
"7d": {
delta: "↑ 3.1%",
points: [["Mon", 9, 6], ["Tue", 12, 7], ["Wed", 8, 11], ["Thu", 14, 9], ["Fri", 17, 12], ["Sat", 6, 4], ["Sun", 5, 3]]
},
"30d": {
delta: "↑ 9.4%",
points: [["Jan", 18, 30], ["Feb", 24, 36], ["Mar", 15, 48], ["Apr", 36, 42], ["May", 30, 60], ["Jun", 48, 48], ["Jul", 42, 72], ["Aug", 60, 60]]
},
"90d": {
delta: "↓ 2.2%",
points: [["Q1 A", 40, 55], ["Q1 B", 52, 61], ["Q2 A", 61, 58], ["Q2 B", 47, 70], ["Q3 A", 66, 64], ["Q3 B", 58, 49]]
}
};
var chart = document.getElementById("chart");
var labels = document.getElementById("chartLabels");
var totalOut = document.getElementById("chartTotal");
var deltaOut = document.getElementById("chartDelta");
var buttons = [].slice.call(document.querySelectorAll("#ranges [data-range]"));
var ACTIVE = "rounded-md bg-white px-2.5 py-1 font-medium text-neutral-900 shadow-sm";
var IDLE = "rounded-md px-2.5 py-1 text-neutral-500";
function draw(range) {
var set = DATA[range];
// Scale against the tallest column so the chart always fills its height.
var tallest = Math.max.apply(
null,
set.points.map(function (p) { return p[1] + p[2]; })
);
chart.innerHTML = "";
labels.innerHTML = "";
var total = 0;
set.points.forEach(function (point) {
var name = point[0];
var fresh = point[1];
var expansion = point[2];
total += fresh + expansion;
// Full-height column so the bars' percentage heights have something to
// resolve against — an items-end row would collapse them to zero.
var column = document.createElement("div");
column.className = "flex h-full flex-1 flex-col justify-end gap-0.5";
column.title = name + " — new " + fresh + "k, expansion " + expansion + "k";
var top = document.createElement("span");
top.className = "rounded-t bg-neutral-900";
top.style.height = (fresh / tallest) * 100 + "%";
var bottom = document.createElement("span");
bottom.className = "rounded-b bg-neutral-300";
bottom.style.height = (expansion / tallest) * 100 + "%";
column.appendChild(top);
column.appendChild(bottom);
chart.appendChild(column);
var label = document.createElement("span");
label.className = "flex-1 text-center";
label.textContent = name;
labels.appendChild(label);
});
totalOut.textContent = "$" + (total * 1000).toLocaleString("en-US");
var up = set.delta.indexOf("↑") === 0;
deltaOut.textContent = set.delta;
deltaOut.className =
"ml-1 rounded px-1.5 py-0.5 text-xs font-medium " +
(up ? "bg-green-100 text-green-700" : "bg-red-100 text-red-700");
buttons.forEach(function (b) {
b.className = b.getAttribute("data-range") === range ? ACTIVE : IDLE;
});
}
buttons.forEach(function (button) {
button.addEventListener("click", function () {
draw(button.getAttribute("data-range"));
});
});
draw("30d");