ChartsCSS
Calendar Heatmap
Contribution-style grid of days with a one-hue ramp, month labels and a hover readout.
Deploys, last 26 weeks
512 deploys in 182 days
| Week of Mon, Apr 27 | 24 deploys |
|---|---|
| Week of Mon, May 4 | 28 deploys |
| Week of Mon, May 11 | 26 deploys |
| Week of Mon, May 18 | 29 deploys |
| Week of Mon, May 25 | 15 deploys |
| Week of Mon, Jun 1 | 7 deploys |
| Week of Mon, Jun 8 | 15 deploys |
| Week of Mon, Jun 15 | 12 deploys |
| Week of Mon, Jun 22 | 22 deploys |
| Week of Mon, Jun 29 | 32 deploys |
| Week of Mon, Jul 6 | 25 deploys |
| Week of Mon, Jul 13 | 24 deploys |
| Week of Mon, Jul 20 | 13 deploys |
| Week of Mon, Jul 27 | 18 deploys |
| Week of Mon, Aug 3 | 8 deploys |
| Week of Mon, Aug 10 | 11 deploys |
| Week of Mon, Aug 17 | 27 deploys |
| Week of Mon, Aug 24 | 24 deploys |
| Week of Mon, Aug 31 | 29 deploys |
| Week of Mon, Sep 7 | 30 deploys |
| Week of Mon, Sep 14 | 12 deploys |
| Week of Mon, Sep 21 | 11 deploys |
| Week of Mon, Sep 28 | 8 deploys |
| Week of Mon, Oct 5 | 18 deploys |
| Week of Mon, Oct 12 | 21 deploys |
| Week of Mon, Oct 19 | 23 deploys |
Installation
npx shadcn@latest add https://ui-collections.vercel.app/r/glass/chart-heatmap.jsonAlso installs the theme, cn() and chart-lib, use-in-view-once. Packages: clsx, tailwind-merge.
Usage
chart-heatmap-demo.tsx
import { ChartHeatmap } from "@/registry/kit/ui/chart-heatmap";
// Deterministic sample: busier mid-week, quiet weekends, a few spikes.
const values = Array.from({ length: 182 }, (_, i) => {
const weekday = (i + 3) % 7;
const base = weekday >= 5 ? 0.6 : 3 + Math.sin(i / 9) * 2;
const spike = i % 23 === 0 ? 6 : 0;
return Math.max(0, Math.round(base + spike + ((i * 37) % 5) - 2));
});
export default function ChartHeatmapDemo() {
return (
<div className="surface w-full max-w-3xl rounded-card p-6">
<h3 className="mb-4 text-sm text-muted">Deploys, last 26 weeks</h3>
<ChartHeatmap values={values} endDate="2026-10-25" label="Production deploys per day over the last 26 weeks" unit="deploys" />
</div>
);
}API
valuesnumber[]One value per day, oldest first.
endDatestringISO date of the last value.
labelstringAccessible name.
unitstringUnit for the readout, e.g. “deploys”.
Source
components/ui/chart-heatmap.tsx
"use client";
import { useRef, useState, type CSSProperties } from "react";
import { useInViewOnce } from "@/registry/kit/hooks/use-in-view-once";
import { cn } from "@/registry/kit/lib/utils";
export interface ChartHeatmapProps {
/** One value per day, oldest first. */
values: number[];
/** ISO date (YYYY-MM-DD) of the last value. */
endDate: string;
label: string;
/** Unit for the readout, e.g. "deploys". */
unit?: string;
className?: string;
}
const day = 86_400_000;
// One hue, faint to bright: low values recede toward the surface.
const levels = [
"color-mix(in oklab, var(--color-fg) 6%, transparent)",
"color-mix(in oklab, var(--color-accent) 22%, transparent)",
"color-mix(in oklab, var(--color-accent) 42%, transparent)",
"color-mix(in oklab, var(--color-accent) 66%, transparent)",
"color-mix(in oklab, var(--color-accent) 92%, transparent)",
];
const dateFormat = new Intl.DateTimeFormat("en-US", { weekday: "short", month: "short", day: "numeric", timeZone: "UTC" });
const monthFormat = new Intl.DateTimeFormat("en-US", { month: "short", timeZone: "UTC" });
/**
* Contribution-style calendar: one cell per day, weeks as columns. Hover a
* cell to read its date and value in the line below the grid.
*/
export function ChartHeatmap({ values, endDate, label, unit = "", className }: ChartHeatmapProps) {
const ref = useRef<HTMLDivElement>(null);
const shown = useInViewOnce(ref);
const [active, setActive] = useState<number | null>(null);
const end = Date.parse(`${endDate}T00:00:00Z`);
const first = end - (values.length - 1) * day;
// Monday-first rows; pad the first column so weekdays line up.
const lead = (new Date(first).getUTCDay() + 6) % 7;
const columns = Math.ceil((lead + values.length) / 7);
const max = Math.max(...values, 1);
const level = (v: number) => (v <= 0 ? 0 : Math.min(4, Math.ceil((v / max) * 4)));
const dateOf = (i: number) => new Date(first + i * day);
const months = Array.from({ length: columns }, (_, col) => {
const i = Math.max(0, col * 7 - lead);
const month = monthFormat.format(dateOf(i));
const prev = col === 0 ? null : monthFormat.format(dateOf(Math.max(0, (col - 1) * 7 - lead)));
return month !== prev ? month : "";
});
const total = values.reduce((a, b) => a + b, 0);
const weeks = Array.from({ length: columns }, (_, col) => {
const from = Math.max(0, col * 7 - lead);
const to = Math.min(values.length, (col + 1) * 7 - lead);
return { start: dateOf(from), sum: values.slice(from, to).reduce((a, b) => a + b, 0) };
});
return (
<div className={cn("grid gap-3", className)}>
<div ref={ref} role="img" aria-label={`${label}. ${total.toLocaleString("en-US")} ${unit} in total.`} onPointerLeave={() => setActive(null)}>
<div className="mb-1.5 grid gap-[3px] text-[10px] text-muted" style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }} aria-hidden>
{months.map((m, i) => (
<span key={i} className="overflow-visible whitespace-nowrap">
{m}
</span>
))}
</div>
<div
aria-hidden
className="grid grid-flow-col grid-rows-7 gap-[3px]"
style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }}
>
{Array.from({ length: lead }, (_, i) => (
<span key={`pad-${i}`} />
))}
{values.map((v, i) => (
<span
key={i}
onPointerEnter={() => setActive(i)}
className={cn(
"aspect-square rounded-[calc(var(--r-field)*0.35)] transition-[opacity,scale] duration-500 ease-theme",
shown ? "scale-100 opacity-100" : "scale-50 opacity-0",
active === i && "ring-1 ring-fg",
)}
style={
{
backgroundColor: levels[level(v)],
transitionDelay: shown ? `${Math.floor((i + lead) / 7) * 12}ms` : "0ms",
} as CSSProperties
}
/>
))}
</div>
</div>
<div className="flex flex-wrap items-center justify-between gap-3 text-xs text-muted">
<span className="min-h-4 tabular-nums">
{active === null ? (
`${total.toLocaleString("en-US")} ${unit} in ${values.length} days`
) : (
<>
<span className="text-fg">{dateFormat.format(dateOf(active))}</span> · {values[active]?.toLocaleString("en-US")} {unit}
</>
)}
</span>
<span className="flex items-center gap-1.5" aria-hidden>
Less
{levels.map((c) => (
<span key={c} className="size-2.5 rounded-[calc(var(--r-field)*0.2)]" style={{ backgroundColor: c }} />
))}
More
</span>
</div>
<div className="sr-only">
<table>
<caption>{label}, weekly totals</caption>
<tbody>
{weeks.map((w) => (
<tr key={w.start.toISOString()}>
<th scope="row">Week of {dateFormat.format(w.start)}</th>
<td>
{w.sum} {unit}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}