For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to any URL or sending an Accept: text/markdown header. The dithercn agent skill, for building any dither on the engine, is at /.well-known/agent-skills/dithercn/SKILL.md.
dithercn

Heatmap

A calendar or matrix heatmap built from dither cells, where every square steps cleanly from a few dots to solid.

Controls
Colour
Steps4
Bloom
Largest cell3px
Week starts
Ripple in

Installation

pnpm dlx shadcn@latest add https://dithercn.dev/r/heatmap.json

Usage

import { Heatmap } from "@/components/dithercn/heatmap";
 
const data = [
  { date: "2026-10-10", value: 3 },
  { date: "2026-10-11", value: 0 },
  { date: "2026-10-12", value: 7 },
];
 
export function Deploys() {
  return <Heatmap data={data} unit={{ one: "deploy", other: "deploys" }} />;
}

Each column is a week and each row a weekday. The grid ends with the latest day in data and goes back weeks weeks, 53 by default. Days missing from data count as 0, and entries for the same day add up. Dates are read in UTC, so a square never moves with the visitor's time zone.

Every square is 4×4 dither cells. With the default four steps a square is dotted, a checkerboard, a hollow frame or solid, and an empty day is a faint backing square. Each level is a fixed mask rather than a raw threshold, so every square keeps a crisp outline and the levels stay distinct even at 2px cells. The scale goes up to six steps.

The grid fills its container. Cells shrink from cell down to 2px to fit every week; below that the oldest weeks drop off, so a phone shows the most recent months.

Hover a square, or focus the grid and use the arrow keys, for a readout above it. Hover a level in the scale to spotlight every square at that level, and click to keep it lit.

Examples

A year of activity

A full year with weeks starting on Monday, in color="purple". On a narrow screen it keeps the latest weeks that fit.

Weekday by hour

mode="matrix" takes a grid of values with row and column labels instead of dates. Here pushes by weekday and hour, with cell={4} for larger squares and steps={5}. Column labels thin out when they would collide.

Props

PropTypeDefault
mode

A day per square, or any grid of values.

Type: "calendar" | "matrix"Default "calendar"
data*

Calendar: values by day, date as "YYYY-MM-DD" or a Date read in UTC. Matrix: data[row][column].

Type: { date: string | Date; value: number }[] | number[][]Default –
weeks

Calendar only: weeks shown, ending with the latest day.

Type: numberDefault 53
weekStart

Calendar only: the first row, 0 for Sunday or 1 for Monday.

Type: 0 | 1Default 0
rows

Matrix only, required: row labels, top to bottom.

Type: string[]Default –
columns

Matrix only, required: column labels, left to right.

Type: string[]Default –
steps

Densities above empty, 2 to 6. The last is solid.

Type: numberDefault 4
max

The value drawn solid. Larger values draw solid too, so one outlier doesn't wash out the rest.

Type: numberDefault The largest value
unit

What a value counts, for the readout and summary: "min" or { one: "deploy", other: "deploys" }.

Type: string | { one: string; other: string }Default –
color

Ink colour, a palette name or any CSS colour.

Type: DitherColorDefault "ink"
bloom

Soft glow behind the two densest levels.

Type: "off" | "low" | "high"Default "off"
cell

Largest dither cell in CSS pixels. Shrinks to 2 to fit the container.

Type: numberDefault --dither-cell
legend

Show the Less to More scale.

Type: booleanDefault true
interactive

Readout, keyboard and the scale spotlight. false draws a static image.

Type: booleanDefault true
animate

Squares ripple in from the top left on first view.

Type: booleanDefault true

Notes

  • The grid is a focusable group labelled with a summary: the range, the total and the busiest square. Arrow keys move a square at a time (left and right move a week in calendar mode), Home and End jump to the ends, and a live region reads each square. Each step in the scale is a toggle button with its range as its name.
  • The ripple plays once when the grid scrolls into view. With reduced motion the grid appears finished and the readout moves without easing.
  • It is one SVG: a backing path and one path per level, built from whole cells snapped to device pixels. Hover only moves an outline and the readout by transform.