A grid of days, a column for each week, each day shaded by how much happened, with the months above, a summary and a scale from less to more.
65 days with work
| Week of | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday |
|---|---|---|---|---|---|---|---|
| Jul 20 | 11 | 17 | 17 | 17 | 14 | 0 | 0 |
| Jul 27 | 7 | 18 | 17 | 11 | 10 | 0 | 0 |
| Aug 3 | 12 | 14 | 11 | 10 | 14 | 6 | 0 |
| Aug 10 | 16 | 14 | 8 | 11 | 18 | 0 | 0 |
| Aug 17 | 0 | 16 | 10 | 7 | 19 | 6 | 4 |
| Aug 24 | 10 | 11 | 17 | 16 | 14 | 0 | 5 |
| Aug 31 | 0 | 12 | 14 | 19 | 0 | 0 | 0 |
| Sep 7 | 11 | 18 | 19 | 13 | 15 | 4 | 7 |
| Sep 14 | 6 | 0 | 7 | 0 | 10 | 0 | 0 |
| Sep 21 | 16 | 0 | 20 | 0 | 0 | 0 | 6 |
| Sep 28 | 13 | 0 | 12 | 8 | 10 | 0 | 3 |
| Oct 5 | 16 | 10 | 15 | 7 | 0 | 6 | 0 |
| Oct 12 | 7 | 12 | 8 | 8 | 9 | 3 | 0 |
Options
The heatmap has no choices: it is drawn one way.
Examples
Usage
Half a year of work, a week to a column, each day shaded from none to most, with the number of days with work and the scale under it.
121 days with work
| Week of | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday |
|---|---|---|---|---|---|---|---|
| Apr 20 | 0 | 0 | 16 | 12 | 9 | 0 | 4 |
| Apr 27 | 0 | 13 | 11 | 13 | 20 | 0 | 4 |
| May 4 | 0 | 0 | 14 | 7 | 14 | 0 | 0 |
| May 11 | 15 | 18 | 0 | 0 | 12 | 0 | 0 |
| May 18 | 11 | 13 | 16 | 11 | 8 | 0 | 4 |
| May 25 | 15 | 7 | 19 | 18 | 19 | 0 | 0 |
| Jun 1 | 18 | 18 | 19 | 14 | 6 | 0 | 0 |
| Jun 8 | 14 | 20 | 18 | 15 | 14 | 0 | 0 |
| Jun 15 | 0 | 0 | 18 | 6 | 14 | 7 | 0 |
| Jun 22 | 15 | 15 | 8 | 13 | 8 | 0 | 0 |
| Jun 29 | 18 | 15 | 15 | 19 | 9 | 0 | 6 |
| Jul 6 | 14 | 13 | 10 | 13 | 0 | 0 | 0 |
| Jul 13 | 0 | 0 | 20 | 0 | 0 | 0 | 0 |
| Jul 20 | 11 | 17 | 17 | 17 | 14 | 0 | 0 |
| Jul 27 | 7 | 18 | 17 | 11 | 10 | 0 | 0 |
| Aug 3 | 12 | 14 | 11 | 10 | 14 | 6 | 0 |
| Aug 10 | 16 | 14 | 8 | 11 | 18 | 0 | 0 |
| Aug 17 | 0 | 16 | 10 | 7 | 19 | 6 | 4 |
| Aug 24 | 10 | 11 | 17 | 16 | 14 | 0 | 5 |
| Aug 31 | 0 | 12 | 14 | 19 | 0 | 0 | 0 |
| Sep 7 | 11 | 18 | 19 | 13 | 15 | 4 | 7 |
| Sep 14 | 6 | 0 | 7 | 0 | 10 | 0 | 0 |
| Sep 21 | 16 | 0 | 20 | 0 | 0 | 0 | 6 |
| Sep 28 | 13 | 0 | 12 | 8 | 10 | 0 | 3 |
| Oct 5 | 16 | 10 | 15 | 7 | 0 | 6 | 0 |
| Oct 12 | 7 | 12 | 8 | 8 | 9 | 3 | 0 |
13 weeks
A quarter: the same parts over 13 weeks.
65 days with work
| Week of | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday |
|---|---|---|---|---|---|---|---|
| Jul 20 | 11 | 17 | 17 | 17 | 14 | 0 | 0 |
| Jul 27 | 7 | 18 | 17 | 11 | 10 | 0 | 0 |
| Aug 3 | 12 | 14 | 11 | 10 | 14 | 6 | 0 |
| Aug 10 | 16 | 14 | 8 | 11 | 18 | 0 | 0 |
| Aug 17 | 0 | 16 | 10 | 7 | 19 | 6 | 4 |
| Aug 24 | 10 | 11 | 17 | 16 | 14 | 0 | 5 |
| Aug 31 | 0 | 12 | 14 | 19 | 0 | 0 | 0 |
| Sep 7 | 11 | 18 | 19 | 13 | 15 | 4 | 7 |
| Sep 14 | 6 | 0 | 7 | 0 | 10 | 0 | 0 |
| Sep 21 | 16 | 0 | 20 | 0 | 0 | 0 | 6 |
| Sep 28 | 13 | 0 | 12 | 8 | 10 | 0 | 3 |
| Oct 5 | 16 | 10 | 15 | 7 | 0 | 6 | 0 |
| Oct 12 | 7 | 12 | 8 | 8 | 9 | 3 | 0 |
Starting midweek
Days before the first value and after the last are left empty, so the weekdays stay in their rows.
60 days with work
| Week of | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday |
|---|---|---|---|---|---|---|---|
| Jul 20 | 17 | 14 | 0 | 0 | |||
| Jul 27 | 7 | 18 | 17 | 11 | 10 | 0 | 0 |
| Aug 3 | 12 | 14 | 11 | 10 | 14 | 6 | 0 |
| Aug 10 | 16 | 14 | 8 | 11 | 18 | 0 | 0 |
| Aug 17 | 0 | 16 | 10 | 7 | 19 | 6 | 4 |
| Aug 24 | 10 | 11 | 17 | 16 | 14 | 0 | 5 |
| Aug 31 | 0 | 12 | 14 | 19 | 0 | 0 | 0 |
| Sep 7 | 11 | 18 | 19 | 13 | 15 | 4 | 7 |
| Sep 14 | 6 | 0 | 7 | 0 | 10 | 0 | 0 |
| Sep 21 | 16 | 0 | 20 | 0 | 0 | 0 | 6 |
| Sep 28 | 13 | 0 | 12 | 8 | 10 | 0 | 3 |
| Oct 5 | 16 | 10 | 15 | 7 | 0 | 6 | 0 |
| Oct 12 | 7 | 12 | 8 | 8 |
Without the legend
The grid alone. A screen reader still hears the summary and reads the table.
65 days with work
| Week of | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday |
|---|---|---|---|---|---|---|---|
| Jul 20 | 11 | 17 | 17 | 17 | 14 | 0 | 0 |
| Jul 27 | 7 | 18 | 17 | 11 | 10 | 0 | 0 |
| Aug 3 | 12 | 14 | 11 | 10 | 14 | 6 | 0 |
| Aug 10 | 16 | 14 | 8 | 11 | 18 | 0 | 0 |
| Aug 17 | 0 | 16 | 10 | 7 | 19 | 6 | 4 |
| Aug 24 | 10 | 11 | 17 | 16 | 14 | 0 | 5 |
| Aug 31 | 0 | 12 | 14 | 19 | 0 | 0 | 0 |
| Sep 7 | 11 | 18 | 19 | 13 | 15 | 4 | 7 |
| Sep 14 | 6 | 0 | 7 | 0 | 10 | 0 | 0 |
| Sep 21 | 16 | 0 | 20 | 0 | 0 | 0 | 6 |
| Sep 28 | 13 | 0 | 12 | 8 | 10 | 0 | 3 |
| Oct 5 | 16 | 10 | 15 | 7 | 0 | 6 | 0 |
| Oct 12 | 7 | 12 | 8 | 8 | 9 | 3 | 0 |
In a narrow space
A narrow layout shows the last 13 weeks and names their months afresh. The table still holds all 26.
121 days with work
| Week of | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday |
|---|---|---|---|---|---|---|---|
| Apr 20 | 0 | 0 | 16 | 12 | 9 | 0 | 4 |
| Apr 27 | 0 | 13 | 11 | 13 | 20 | 0 | 4 |
| May 4 | 0 | 0 | 14 | 7 | 14 | 0 | 0 |
| May 11 | 15 | 18 | 0 | 0 | 12 | 0 | 0 |
| May 18 | 11 | 13 | 16 | 11 | 8 | 0 | 4 |
| May 25 | 15 | 7 | 19 | 18 | 19 | 0 | 0 |
| Jun 1 | 18 | 18 | 19 | 14 | 6 | 0 | 0 |
| Jun 8 | 14 | 20 | 18 | 15 | 14 | 0 | 0 |
| Jun 15 | 0 | 0 | 18 | 6 | 14 | 7 | 0 |
| Jun 22 | 15 | 15 | 8 | 13 | 8 | 0 | 0 |
| Jun 29 | 18 | 15 | 15 | 19 | 9 | 0 | 6 |
| Jul 6 | 14 | 13 | 10 | 13 | 0 | 0 | 0 |
| Jul 13 | 0 | 0 | 20 | 0 | 0 | 0 | 0 |
| Jul 20 | 11 | 17 | 17 | 17 | 14 | 0 | 0 |
| Jul 27 | 7 | 18 | 17 | 11 | 10 | 0 | 0 |
| Aug 3 | 12 | 14 | 11 | 10 | 14 | 6 | 0 |
| Aug 10 | 16 | 14 | 8 | 11 | 18 | 0 | 0 |
| Aug 17 | 0 | 16 | 10 | 7 | 19 | 6 | 4 |
| Aug 24 | 10 | 11 | 17 | 16 | 14 | 0 | 5 |
| Aug 31 | 0 | 12 | 14 | 19 | 0 | 0 | 0 |
| Sep 7 | 11 | 18 | 19 | 13 | 15 | 4 | 7 |
| Sep 14 | 6 | 0 | 7 | 0 | 10 | 0 | 0 |
| Sep 21 | 16 | 0 | 20 | 0 | 0 | 0 | 6 |
| Sep 28 | 13 | 0 | 12 | 8 | 10 | 0 | 3 |
| Oct 5 | 16 | 10 | 15 | 7 | 0 | 6 | 0 |
| Oct 12 | 7 | 12 | 8 | 8 | 9 | 3 | 0 |
Guidelines
Purpose
Shows how much happened on each day over a quarter or a half year, a square for each day shaded from less to more, so a reader sees busy weeks, quiet ones and the rhythm of the week at a glance. It differs from a bar chart, which compares totals a reader can read off.
When to use
- How much a person or a team worked, published or changed, day by day, over 13 or 26 weeks.
- A pattern across the days of the week, such as quiet weekends.
When not to use
Usage rules
Do
- Say what the values are in the label and the title, with their unit.
- Give a summary that answers the reader's first question.
- Give heatmaps side by side one highest value, so their shades compare.
Don't
- Use the heatmap's shades for anything but more and less.
- Leave out the label: it titles the table a screen reader reads.
65 days with work
| Week of | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday |
|---|---|---|---|---|---|---|---|
| Jul 20 | 11 | 17 | 17 | 17 | 14 | 0 | 0 |
| Jul 27 | 7 | 18 | 17 | 11 | 10 | 0 | 0 |
| Aug 3 | 12 | 14 | 11 | 10 | 14 | 6 | 0 |
| Aug 10 | 16 | 14 | 8 | 11 | 18 | 0 | 0 |
| Aug 17 | 0 | 16 | 10 | 7 | 19 | 6 | 4 |
| Aug 24 | 10 | 11 | 17 | 16 | 14 | 0 | 5 |
| Aug 31 | 0 | 12 | 14 | 19 | 0 | 0 | 0 |
| Sep 7 | 11 | 18 | 19 | 13 | 15 | 4 | 7 |
| Sep 14 | 6 | 0 | 7 | 0 | 10 | 0 | 0 |
| Sep 21 | 16 | 0 | 20 | 0 | 0 | 0 | 6 |
| Sep 28 | 13 | 0 | 12 | 8 | 10 | 0 | 3 |
| Oct 5 | 16 | 10 | 15 | 7 | 0 | 6 | 0 |
| Oct 12 | 7 | 12 | 8 | 8 | 9 | 3 | 0 |
Give a summary that answers the first question, and say in the label what is counted.
| Week of | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday |
|---|---|---|---|---|---|---|---|
| Jul 20 | 11 | 17 | 17 | 17 | 14 | 0 | 0 |
| Jul 27 | 7 | 18 | 17 | 11 | 10 | 0 | 0 |
| Aug 3 | 12 | 14 | 11 | 10 | 14 | 6 | 0 |
| Aug 10 | 16 | 14 | 8 | 11 | 18 | 0 | 0 |
| Aug 17 | 0 | 16 | 10 | 7 | 19 | 6 | 4 |
| Aug 24 | 10 | 11 | 17 | 16 | 14 | 0 | 5 |
| Aug 31 | 0 | 12 | 14 | 19 | 0 | 0 | 0 |
| Sep 7 | 11 | 18 | 19 | 13 | 15 | 4 | 7 |
| Sep 14 | 6 | 0 | 7 | 0 | 10 | 0 | 0 |
| Sep 21 | 16 | 0 | 20 | 0 | 0 | 0 | 6 |
| Sep 28 | 13 | 0 | 12 | 8 | 10 | 0 | 3 |
| Oct 5 | 16 | 10 | 15 | 7 | 0 | 6 | 0 |
| Oct 12 | 7 | 12 | 8 | 8 | 9 | 3 | 0 |
Without a summary the reader has to count the squares.
Anatomy
- The block, as wide as its weeks.
- The chart part: the weekdays part at the left, naming Monday, Wednesday and Friday in weekday parts, then a week part for each week, Monday at the top. Each week holds a month part, naming the month above the week it starts in, and a cell part for each day. A cell's level is a modifier of the part: level 0 for nothing, then levels 1 to 4 in the sequential data colors from light to strong; a blank cell is a day outside the data.
- The legend part: the summary part at the left, such as "141 days with work", and the scale part at the right, the words less and more around one square of each level.
- The table part: the same values, a week to a row, for screen readers only.
Behavior
The wrapper shades each day by a quarter of the highest value per level, or of a highest value the page gives, so several heatmaps can share one scale. A week runs Monday to Sunday; days before the first value and after the last are left empty. In a narrow layout the heatmap shows its last 13 weeks, naming the months as if they began there; the table keeps every week. The wrapper writes the month and weekday names and the table's values in English unless locale names another language (or format writes the values), so a page prepared on a server shows the same words once its script runs. Pointing at a day shows the browser's own tooltip, with its date and value; hand-written markup puts the same words in a title on the day. Nothing moves and nothing takes focus.
Accessibility
What it does for you
- The picture is hidden from screen readers. They read a table instead, titled by the heatmap's label, with a row for each week and a column for each day, holding each day's value.
- The summary is in words under the grid and read in its place.
- The shades are never the only way to read a value: the table holds them all. The lightest shades are faint against the page, which is why the table is always there.
- In high contrast mode the squares keep their shades, so the scale still reads from less to more.
- Nothing in it takes focus.
What you must do
- Give a label: it titles the table a screen reader reads.
- Give the summary the fact that matters, such as how many days had work.
- In markup written by hand, keep the table and hide the chart part and the scale part.
How to test
- With a screen reader, the picture is skipped, the table is read by its label with each week and day's value, and the summary is read in words.
- With the colors removed, the table and the summary still hold every value.
- In a forced-color mode, the squares keep their shades and the scale reads from less to more.
- With text at 200% and the page 320 pixels wide, the heatmap shows its last 13 weeks and the legend stays whole.
Source
The files the heatmap is made of. Its folder on GitHub (opens in a new tab) holds them with its wrapper and its document.
Stylesheet
/* smbk-heatmap — Heatmap. Figma "Heatmap" 7580:375.
* A column for each week, Monday at the top, each day a small square in its level's shade: the track's faint
* gray for none, then four steps of the sequential data color from less to more. Monday, Wednesday and Friday
* are named at the left and each month above the week it starts in. Under the grid, the summary at the left and
* the scale from less to more at the right. Days outside the data are left empty. The picture is hidden from
* screen readers, which read the same values in a table kept out of sight. In a narrow layout the heatmap shows
* its last 13 weeks; the table keeps them all. */
.smbk-heatmap {
display: flex;
flex-direction: column;
gap: var(--smbk-space-stack-sm);
width: max-content;
max-width: 100%;
color: var(--smbk-color-text-subtle);
font-family: var(--smbk-typography-body-xs-font-family);
font-size: var(--smbk-typography-body-xs-font-size);
font-weight: var(--smbk-typography-body-xs-font-weight);
line-height: var(--smbk-typography-body-xs-line-height);
letter-spacing: var(--smbk-typography-body-xs-letter-spacing);
text-transform: var(--smbk-typography-body-xs-text-transform);
}
/* The block frames the words it hides for screen readers, so they stay inside it wherever it is placed: without a
frame they hang from the page itself and stretch it, past the end of an area that scrolls. No weight, so a parent
that places the block keeps its say. */
:where(.smbk-heatmap) {
position: relative;
}
.smbk-heatmap__chart {
display: flex;
gap: var(--smbk-space-4);
}
.smbk-heatmap__weekdays,
.smbk-heatmap__week {
display: flex;
flex: none;
flex-direction: column;
gap: var(--smbk-space-4);
}
.smbk-heatmap__weekdays {
width: var(--smbk-size-28);
}
/* The month row: as tall as a line of text, a step further from the grid than the days are from each other.
A name starts over its week and runs on over the next weeks, whose month slots are empty. */
.smbk-heatmap__month {
display: block;
width: var(--smbk-size-12);
height: calc(var(--smbk-typography-body-xs-font-size) * var(--smbk-typography-body-xs-line-height));
margin-bottom: calc(var(--smbk-space-stack-sm) - var(--smbk-space-4));
white-space: nowrap;
}
.smbk-heatmap__weekdays .smbk-heatmap__month {
width: auto;
}
.smbk-heatmap__month-name--narrow {
display: none;
}
/* A weekday's name is centred on its row of squares, whose height it does not change. */
.smbk-heatmap__weekday {
display: flex;
align-items: center;
height: var(--smbk-size-12);
white-space: nowrap;
}
.smbk-heatmap__cell {
display: block;
flex: none;
width: var(--smbk-size-12);
height: var(--smbk-size-12);
border-radius: var(--smbk-radius-2);
background: var(--smbk-color-surface-track-subtle);
}
.smbk-heatmap__cell--level-1 { background: var(--smbk-color-data-sequential-2); }
.smbk-heatmap__cell--level-2 { background: var(--smbk-color-data-sequential-3); }
.smbk-heatmap__cell--level-3 { background: var(--smbk-color-data-sequential-4); }
.smbk-heatmap__cell--level-4 { background: var(--smbk-color-data-sequential-5); }
.smbk-heatmap__cell--blank {
background: none;
}
.smbk-heatmap__legend {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: var(--smbk-space-stack-xs) var(--smbk-space-inline-lg);
}
.smbk-heatmap__summary {
margin: 0;
}
.smbk-heatmap__scale {
display: flex;
align-items: center;
gap: var(--smbk-space-4);
}
/* The table and a summary without its legend, for screen readers only. */
/* .smbk-heatmap__table and .smbk-heatmap__hidden: hidden from view, kept for screen readers and the keyboard, by the shared rule in visually-hidden/visually-hidden.css. */
/* A table takes the room its rows need whatever width and height it is given, so the hidden one is full size, only
unseen. It hangs from the block's end corner, reaching back and up, the way a page never scrolls: it adds no room
to the block or to any area that scrolls around it, and stays a table for screen readers. */
.smbk-heatmap__table {
inset-block-end: 0;
inset-inline-end: 0;
}
/* A narrow space shows the last 13 weeks, with the months named as if they began there. */
@container style(--smbk-layout: narrow) {
.smbk-heatmap__week--early,
.smbk-heatmap__month-name--wide {
display: none;
}
.smbk-heatmap__month-name--narrow {
display: inline;
}
}
/* ---- high contrast (forced colors) ---- */
/* The squares keep their shades, so the scale still reads from less to more; the table holds every value. */
@media (forced-colors: active) {
.smbk-heatmap__cell {
forced-color-adjust: none;
}
}Tokens it reads
| Token and CSS | Value | Preview | Foundation |
|---|---|---|---|
| space.stack.sm--smbk-space-stack-sm | comfortable: 8pxcompact: 6pxchanges with density | Spacing | |
| color.text.subtle--smbk-color-text-subtle | light: #4e555edark: #9ba5b1changes with theme | Color | |
| typography.body-xs--smbk-typography-body-xs-font-family--smbk-typography-body-xs-font-size--smbk-typography-body-xs-font-weight--smbk-typography-body-xs-line-height--smbk-typography-body-xs-letter-spacing--smbk-typography-body-xs-text-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 12pxfontWeight: 400lineHeight: 1.5letterSpacing: 0px | The quick brown fox | Typography |
| space.4--smbk-space-4 | 4px | Spacing | |
| size.28--smbk-size-28 | 28px | Size | |
| size.12--smbk-size-12 | 12px | Size | |
| radius.2--smbk-radius-2 | 2px | Radius | |
| color.surface.track-subtle--smbk-color-surface-track-subtle | light: #c4cdd8dark: #2d3237changes with theme | Color | |
| color.data.sequential.2--smbk-color-data-sequential-2 | light: default: #7fa0ff · cockpit: #7ebcd0dark: default: #2f4ab8 · cockpit: #1d5e70changes with theme | Color | |
| color.data.sequential.3--smbk-color-data-sequential-3 | light: default: #557bff · cockpit: #389db9dark: default: #3b5ce3 · cockpit: #1d7a93changes with theme | Color | |
| color.data.sequential.4--smbk-color-data-sequential-4 | light: default: #3b5ce3 · cockpit: #027b96dark: default: #557bff · cockpit: #3097b3changes with theme | Color | |
| color.data.sequential.5--smbk-color-data-sequential-5 | light: default: #2f4ab8 · cockpit: #01596ddark: default: #b8ccff · cockpit: #64b8d2changes with theme | Color | |
| space.stack.xs--smbk-space-stack-xs | 4px | Spacing | |
| space.inline.lg--smbk-space-inline-lg | comfortable: 12pxcompact: 8pxchanges with density | Spacing |
Manifest
{
"name": "heatmap",
"block": "smbk-heatmap",
"summary": "A grid of days, a column for each week, each day shaded by how much happened, with the months above, a summary and a scale from less to more.",
"adapt": "css",
"axes": {},
"flags": {},
"parts": [
"chart",
"weekdays",
"weekday",
"week",
"month",
"month-name",
"cell",
"legend",
"summary",
"scale",
"scale-word",
"table",
"hidden"
],
"figmaNodeId": "7580:375"
}