Flux 2.0

Component

Heatmap

A matrix of values on one sequential scale, where every cell can be focused and opened for its detail.

Plain-text source, for agents and clipboard use: /components/source/heatmap.txt

When to use it

Shows where values run high or low across two dimensions at once: occupancy by tour and month, a P&L line by period, a slice of usage by team and week. It is a real table, so the row and column headers name every cell, and each value is a button filled with one step of a five-step teal ramp and printed in the cell, so the colour is never the only way to read it. The grid is one tab stop: the arrow keys move between cells, and activating one opens its detail, typically in a Drawer.

Not this
Not for a single series over time; use a Chart. Not for values that are categories rather than amounts, such as a status per day; use a Table with Badges. Not when people need to edit the values; use Editable cells in a Table. Not for more than about twelve columns at once; aggregate, or let the table scroll horizontally.

Examples

Occupancy by tour and month

Tab into the grid, move with the arrow keys and press Enter, or click a cell: the drawer opens on that tour and month. Empty cells are tours that do not run that month.

Occupancy by tour and month

Every tour peaks in July and August. Westfjords and the Highlands stay below 80% outside them.

AprMayJunJulAugSep
Golden Circle
South Coast
Snæfellsnes
WestfjordsNo departures
HighlandsNo departuresNo departures
  • Under 50%
  • 50–64%
  • 65–79%
  • 80–89%
  • 90% and over

Golden Circle

Apr · 72% occupancy

Tour
Golden Circle
Month
Apr
Occupancy
72%
html
<figure class="flux-heatmap" data-flux-heatmap>
  <figcaption class="flux-heatmap__header">
    <h3 class="flux-heatmap__title" id="occupancy-heatmap-title">Occupancy by tour and month</h3>
    <p class="flux-heatmap__summary">Every tour peaks in July and August. Westfjords and the Highlands stay below 80% outside them.</p>
  </figcaption>
  <div class="flux-heatmap__scroll">
    <table class="flux-heatmap__table" aria-labelledby="occupancy-heatmap-title">
      <thead>
        <tr><td></td><th scope="col">Apr</th><th scope="col">May</th><th scope="col">Jun</th><th scope="col">Jul</th><th scope="col">Aug</th><th scope="col">Sep</th></tr>
      </thead>
      <tbody>
        <tr><th scope="row">Golden Circle</th><td><button class="flux-heatmap__cell" type="button" data-step="3" tabindex="0" aria-label="Golden Circle, Apr: 72%" commandfor="occupancy-detail" command="show-modal">72%</button></td><td><button class="flux-heatmap__cell" type="button" data-step="4" tabindex="-1" aria-label="Golden Circle, May: 81%" commandfor="occupancy-detail" command="show-modal">81%</button></td><td><button class="flux-heatmap__cell" type="button" data-step="4" tabindex="-1" aria-label="Golden Circle, Jun: 88%" commandfor="occupancy-detail" command="show-modal">88%</button></td><td><button class="flux-heatmap__cell" type="button" data-step="5" tabindex="-1" aria-label="Golden Circle, Jul: 94%" commandfor="occupancy-detail" command="show-modal">94%</button></td><td><button class="flux-heatmap__cell" type="button" data-step="5" tabindex="-1" aria-label="Golden Circle, Aug: 96%" commandfor="occupancy-detail" command="show-modal">96%</button></td><td><button class="flux-heatmap__cell" type="button" data-step="4" tabindex="-1" aria-label="Golden Circle, Sep: 85%" commandfor="occupancy-detail" command="show-modal">85%</button></td></tr>
        <tr><th scope="row">South Coast</th><td><button class="flux-heatmap__cell" type="button" data-step="2" tabindex="-1" aria-label="South Coast, Apr: 64%" commandfor="occupancy-detail" command="show-modal">64%</button></td><td><button class="flux-heatmap__cell" type="button" data-step="3" tabindex="-1" aria-label="South Coast, May: 75%" commandfor="occupancy-detail" command="show-modal">75%</button></td><td><button class="flux-heatmap__cell" type="button" data-step="4" tabindex="-1" aria-label="South Coast, Jun: 86%" commandfor="occupancy-detail" command="show-modal">86%</button></td><td><button class="flux-heatmap__cell" type="button" data-step="5" tabindex="-1" aria-label="South Coast, Jul: 91%" commandfor="occupancy-detail" command="show-modal">91%</button></td><td><button class="flux-heatmap__cell" type="button" data-step="5" tabindex="-1" aria-label="South Coast, Aug: 93%" commandfor="occupancy-detail" command="show-modal">93%</button></td><td><button class="flux-heatmap__cell" type="button" data-step="3" tabindex="-1" aria-label="South Coast, Sep: 79%" commandfor="occupancy-detail" command="show-modal">79%</button></td></tr>
        <tr><th scope="row">Snæfellsnes</th><td><button class="flux-heatmap__cell" type="button" data-step="1" tabindex="-1" aria-label="Snæfellsnes, Apr: 48%" commandfor="occupancy-detail" command="show-modal">48%</button></td><td><button class="flux-heatmap__cell" type="button" data-step="2" tabindex="-1" aria-label="Snæfellsnes, May: 62%" commandfor="occupancy-detail" command="show-modal">62%</button></td><td><button class="flux-heatmap__cell" type="button" data-step="3" tabindex="-1" aria-label="Snæfellsnes, Jun: 77%" commandfor="occupancy-detail" command="show-modal">77%</button></td><td><button class="flux-heatmap__cell" type="button" data-step="4" tabindex="-1" aria-label="Snæfellsnes, Jul: 85%" commandfor="occupancy-detail" command="show-modal">85%</button></td><td><button class="flux-heatmap__cell" type="button" data-step="4" tabindex="-1" aria-label="Snæfellsnes, Aug: 88%" commandfor="occupancy-detail" command="show-modal">88%</button></td><td><button class="flux-heatmap__cell" type="button" data-step="3" tabindex="-1" aria-label="Snæfellsnes, Sep: 66%" commandfor="occupancy-detail" command="show-modal">66%</button></td></tr>
        <tr><th scope="row">Westfjords</th><td><span class="flux-heatmap__empty"><span aria-hidden="true">–</span><span class="flux-visually-hidden">No departures</span></span></td><td><button class="flux-heatmap__cell" type="button" data-step="1" tabindex="-1" aria-label="Westfjords, May: 41%" commandfor="occupancy-detail" command="show-modal">41%</button></td><td><button class="flux-heatmap__cell" type="button" data-step="2" tabindex="-1" aria-label="Westfjords, Jun: 58%" commandfor="occupancy-detail" command="show-modal">58%</button></td><td><button class="flux-heatmap__cell" type="button" data-step="3" tabindex="-1" aria-label="Westfjords, Jul: 73%" commandfor="occupancy-detail" command="show-modal">73%</button></td><td><button class="flux-heatmap__cell" type="button" data-step="3" tabindex="-1" aria-label="Westfjords, Aug: 76%" commandfor="occupancy-detail" command="show-modal">76%</button></td><td><button class="flux-heatmap__cell" type="button" data-step="2" tabindex="-1" aria-label="Westfjords, Sep: 52%" commandfor="occupancy-detail" command="show-modal">52%</button></td></tr>
        <tr><th scope="row">Highlands</th><td><span class="flux-heatmap__empty"><span aria-hidden="true">–</span><span class="flux-visually-hidden">No departures</span></span></td><td><span class="flux-heatmap__empty"><span aria-hidden="true">–</span><span class="flux-visually-hidden">No departures</span></span></td><td><button class="flux-heatmap__cell" type="button" data-step="3" tabindex="-1" aria-label="Highlands, Jun: 69%" commandfor="occupancy-detail" command="show-modal">69%</button></td><td><button class="flux-heatmap__cell" type="button" data-step="4" tabindex="-1" aria-label="Highlands, Jul: 82%" commandfor="occupancy-detail" command="show-modal">82%</button></td><td><button class="flux-heatmap__cell" type="button" data-step="4" tabindex="-1" aria-label="Highlands, Aug: 87%" commandfor="occupancy-detail" command="show-modal">87%</button></td><td><button class="flux-heatmap__cell" type="button" data-step="2" tabindex="-1" aria-label="Highlands, Sep: 54%" commandfor="occupancy-detail" command="show-modal">54%</button></td></tr>
      </tbody>
    </table>
  </div>
  <ul class="flux-heatmap__key" aria-label="Occupancy scale">
    <li><span class="flux-heatmap__swatch" data-step="1"></span>Under 50%</li>
    <li><span class="flux-heatmap__swatch" data-step="2"></span>50–64%</li>
    <li><span class="flux-heatmap__swatch" data-step="3"></span>65–79%</li>
    <li><span class="flux-heatmap__swatch" data-step="4"></span>80–89%</li>
    <li><span class="flux-heatmap__swatch" data-step="5"></span>90% and over</li>
  </ul>
</figure>
<dialog class="flux-drawer" id="occupancy-detail" aria-labelledby="occupancy-detail-title">
  <div class="flux-drawer__header">
    <div class="flux-drawer__heading">
      <h2 class="flux-drawer__title" id="occupancy-detail-title" data-flux-heatmap-field="row">Golden Circle</h2>
      <p class="flux-drawer__meta"><span data-flux-heatmap-field="column">Apr</span> · <span data-flux-heatmap-field="value">72%</span> occupancy</p>
    </div>
    <button class="flux-drawer__close" type="button" commandfor="occupancy-detail" command="close" aria-label="Close"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" /></svg></button>
  </div>
  <div class="flux-drawer__body">
    <dl class="flux-dl">
      <div class="flux-dl__pair"><dt class="flux-dl__term">Tour</dt><dd class="flux-dl__detail" data-flux-heatmap-field="row">Golden Circle</dd></div>
      <div class="flux-dl__pair"><dt class="flux-dl__term">Month</dt><dd class="flux-dl__detail" data-flux-heatmap-field="column">Apr</dd></div>
      <div class="flux-dl__pair"><dt class="flux-dl__term">Occupancy</dt><dd class="flux-dl__detail flux-dl__detail--data" data-flux-heatmap-field="value">72%</dd></div>
    </dl>
  </div>
  <div class="flux-drawer__footer">
    <button class="flux-button flux-button--ghost" type="button" commandfor="occupancy-detail" command="close">Close</button>
    <a class="flux-button" href="#">Open departures</a>
  </div>
</dialog>

Anatomy

PartDescription
CaptionA heading naming what is measured and a sentence stating the pattern, as on a Chart.
TableA table labelled by the heading. Column headers with scope="col" in mono, row headers with scope="row".
CellA button per value with data-step from 1 to 5, filled with chart-sequential-<step> and printed in chart-sequential-text-<step>, audited at 4.5:1 in every theme.
Empty cellA span with no fill, a dash and hidden text, for a combination with no value, so it never reads as a low one. It is skipped by the arrow keys.
KeyBelow the table, a swatch and the range of each step, such as "80–89%".
DetailA Drawer the cells open with commandfor. Elements in it with data-flux-heatmap-field="row", "column" or "value" are filled from the cell before it opens.

API

NameKindDescription
flux-heatmapclassOn a figure, with data-flux-heatmap. It has no surface of its own; place it in a card or a section.
data-flux-heatmapattributeBinds the arrow keys, the roving tabindex and the detail fields. Requires flux-components.js.
flux-heatmap__header | __title | __summaryclassThe figcaption, its heading and the pattern in words.
flux-heatmap__scroll | __tableclassThe horizontal scroll container and the table, labelled with aria-labelledby.
flux-heatmap__cellclassA button per value with data-step, an aria-label naming the row, the column and the value, and tabindex="0" on the first cell and "-1" on the rest.
data-stepattribute1 to 5, on a cell or a key swatch: the step of the sequential scale.
flux-heatmap__emptyclassA span in a cell with no value.
flux-heatmap__key | __swatchclassThe scale key and its swatches.
data-flux-heatmap-fieldattribute"row", "column" or "value", on elements in the dialog a cell opens.
flux-heatmap-selectelementEvent dispatched from the heatmap when a cell is activated, with detail.row, detail.column, detail.value and detail.cell, for loading the record behind it.

States

StateTreatment
Stepschart-sequential-1 to -5, lightest to darkest in light and darkest to lightest in dark and deep, so a high value always stands out most from the page. On deep the ramp steps around the teal ground.
HoverAn inset ring in border-strong.
FocusThe Flux focus ring, raised over the neighbouring cells.
EmptyNo fill, a dash in text-secondary, and "No departures" or similar in hidden text.

Keyboard

KeysAction
TabMoves into the grid, to the last cell used, and out again.
Arrow keysMove one cell in that direction, skipping empty cells.
Home / EndMove to the first or last cell in the row.
Ctrl + Home / Ctrl + EndMove to the first or last cell in the grid. ⌘ on macOS.
Enter / SpaceOpens the cell’s detail.

Accessibility

  • A native table with scoped headers, so screen readers can also move by row and column with their own table commands.
  • Each cell button names its row, column and value in its aria-label: "Golden Circle, Jul: 94%".
  • The value is printed in every cell; the colour is a second signal, never the only one.
  • The detail opens in a Drawer on the native dialog, so focus moves into it and returns to the cell when it closes.

Tokens

The tier-3 and tier-2 tokens this component binds to. Change the token, not the component.

  • --flux-chart-sequential-1 to -5 and --flux-chart-sequential-text-1 to -5 (Tier 2)
  • --flux-heatmap-cell-height | -cell-min-width | -cell-gap | -cell-padding-x
  • --flux-heatmap-header | -row-header | -empty-text | -hover-ring | -key-swatch

Do and don’t

Do

  • State the pattern in the summary sentence, so nobody has to scan the grid to find it.
  • Choose step boundaries that mean something to the reader, and say them in the key.
  • Open the detail with the cell’s context in its title: the row, then the column.

Don’t

  • Do not use status or product colours as steps. The ramp is one teal, low to high.
  • Do not hide the values to make the grid look cleaner.
  • Do not put two measures in one heatmap. Make two.