Skip to content
ShineUI
Install

Heatmap

Cells on a single-hue scale.

<sui-chart-heatmap>ChartsStable

About

Density matrix of rows by columns, for when two categories cross and the density of the crossing matters: open receivables by customer and aging bucket, activity by team and week. Cells step through one hue, the surface to the first ramp step, and the largest value gets the one accent ring. Hovering a cell shows the exact figure.

Usage

<sui-chart-heatmap heading="Open A/R by customer and aging bucket" subtitle="Single-hue navy scale" caption="Orange ring marks the largest exposure · hover any cell for the exact figure" money></sui-chart-heatmap> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-chart-heatmap"); el.cols = ["Current", "1–30", "31–60", "61+"]; /* string[] */ el.rows = ["Clark County", "Derby City", "Riverbend", "New Albany Parks", "Huber Winery", "Kye's Catering"]; /* string[] */ el.addEventListener("suiHover", (e) => console.log(e.detail)); </script>
import { SuiChartHeatmap } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const cols = ["Current", "1–30", "31–60", "61+"]; /* string[] */ const rows = ["Clark County", "Derby City", "Riverbend", "New Albany Parks", "Huber Winery", "Kye's Catering"]; /* string[] */ return <SuiChartHeatmap heading="Open A/R by customer and aging bucket" subtitle="Single-hue navy scale" caption="Orange ring marks the largest exposure · hover any cell for the exact figure" money cols={cols} rows={rows} onSuiHover={(e) => console.log(e.detail)} />; }
<script setup> import { SuiChartHeatmap } from "@shineui/vue"; import "@shineui/core/shineui.css"; const cols = ["Current", "1–30", "31–60", "61+"]; // string[] const rows = ["Clark County", "Derby City", "Riverbend", "New Albany Parks", "Huber Winery", "Kye's Catering"]; // string[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiChartHeatmap heading="Open A/R by customer and aging bucket" subtitle="Single-hue navy scale" caption="Orange ring marks the largest exposure · hover any cell for the exact figure" money :cols="cols" :rows="rows" @sui-hover="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiChartHeatmap } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiChartHeatmap], template: `<sui-chart-heatmap heading="Open A/R by customer and aging bucket" subtitle="Single-hue navy scale" caption="Orange ring marks the largest exposure · hover any cell for the exact figure" money [cols]="cols" [rows]="rows" (suiHover)="onChange($event)"></sui-chart-heatmap>`, }) export class Example { cols = ["Current", "1–30", "31–60", "61+"]; // string[] rows = ["Clark County", "Derby City", "Riverbend", "New Albany Parks", "Huber Winery", "Kye's Catering"]; // string[] onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

values[row][col] holds the figures. The matrix is a read-only data grid, named by an aria-label on the host or the heading, with a columnheader per bucket, a rowheader per customer, and a name of customer, bucket and figure on every cell. Cells mix --viz-1 into --surface by share of the largest.

<sui-chart-heatmap heading="Open A/R by customer and aging bucket" subtitle="Single-hue navy scale" caption="Orange ring marks the largest exposure · hover any cell for the exact figure" money></sui-chart-heatmap>
// setup(root): properties and events setup(root) { fillAr(root.querySelector('sui-chart-heatmap')); }

Counts

Without money the cells, the scale and the tooltip print plain counts, which is the default; the money examples above set the attribute. The scale still runs from 0 to the largest value in the matrix. With money, currency="EUR" prints euros the way the page's locale writes them.

<sui-chart-heatmap heading="Invoices posted by clerk and week" subtitle="Six weeks to Aug 8, 2026"></sui-chart-heatmap>
// setup(root): properties and events setup(root) { fillPosted(root.querySelector('sui-chart-heatmap')); }

Compact cells

cell is the cell width in px and the height is cell minus 8. Below 32 the cells drop their printed figure, which keeps a wide matrix legible; the figure stays in the tooltip and in the cell accessible name.

<sui-chart-heatmap heading="Invoices posted by clerk and week" subtitle="Compact cells at 26px" cell="26"></sui-chart-heatmap>
// setup(root): properties and events setup(root) { fillPosted(root.querySelector('sui-chart-heatmap')); }

Peak marker off

hide-max drops the orange ring and the peak note under the scale, for a matrix where no single cell is the story. The header-end slot puts a control beside the heading.

<sui-chart-heatmap heading="Open A/R by customer and aging bucket" subtitle="No peak marker" hide-max money> <sui-button slot="header-end" size="sm" variant="ghost" label="Export"></sui-button> </sui-chart-heatmap>
// setup(root): properties and events setup(root) { fillAr(root.querySelector('sui-chart-heatmap')); }

Events

suiHover fires when the hovered cell changes and reports { index, row, col }: index counts cells row by row, and all three are -1 on leave. hoverIndex takes that same index, so a write from outside rings the cell and shows its figure without emitting.

Idle.
<div style="display:flex;flex-direction:column;gap:12px"> <sui-chart-heatmap id="hm-events" heading="Open A/R by customer and aging bucket" subtitle="Hover a cell" money></sui-chart-heatmap> <span id="hm-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Idle.</span> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#hm-events'); const log = root.querySelector('#hm-log'); fillAr(el); el.addEventListener('suiHover', (e) => { const { index, row, col } = e.detail; log.textContent = index < 0 ? 'Idle.' : `Cell ${index}: ${AR_ROWS[row]}, ${AR_COLS[col]}, $${AR_VALUES[row][col].toLocaleString('en-US')}.`; }); }

Empty and loading

loading stands a shimmer in for the plot and marks the host aria-busy while the figures are on their way; the frame and its heading stay. A chart with nothing to plot keeps its frame and says so, and emptyMessage is the sentence. Both are the same on every chart.

<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px"> <sui-chart-heatmap id="hm-loading" heading="Open A/R by customer and aging bucket" subtitle="Fetching the ledger" loading></sui-chart-heatmap> <sui-chart-heatmap id="hm-empty" heading="Open A/R by customer and aging bucket" subtitle="Nothing open" empty-message="No receivables are open."></sui-chart-heatmap> </div>
// setup(root): properties and events setup(root) { for (const id of ['#hm-loading', '#hm-empty']) { const el = root.querySelector(id); el.rows = ['Clark County', 'Derby City', 'Riverbend']; el.cols = ['Current', '1–30', '31–60', '61+']; } }

Keyboard

KeyAction
TabMoves to the matrix, a read-only data grid with one tab stop.
ArrowRight ArrowLeftMoves to the next or the previous cell in the row and shows its card; a reader hears the row, the column and the exact figure.
ArrowDown ArrowUpMoves to the cell below or above.
Home EndMoves to the first or the last cell of the row.
Ctrl+Home Ctrl+EndMoves to the first or the last cell of the matrix.
EscapeLeaves the cell and hides its card; the matrix keeps focus.

API

<sui-chart-heatmap>

Properties

PropertyAttributeTypeDefaultDescription
captioncaptionstringstring | undefinedFooter note, separated by a hairline.
cellcellnumber34Cell width in px; height is cell - 8. Below 32 the cells drop their text, and the figure stays in the tooltip and in the cell's accessible name. A printed figure takes whichever of --ink, --ink-on-dark and --surface reads highest on that cell's own mixed color, measured at render, so the light and dark themes both cross over where the contrast says to.
colsproperty onlystring[][]Column labels (top). Each becomes a columnheader. Property, not attribute.
currencycurrencystring'USD'The ISO 4217 code money prints in: USD by default, EUR, JPY. The page's locale (setLocale(), else <html lang>) places the symbol and sets the grouping and the decimal mark, so EUR on a German page reads 19.000 € in a cell and 18.950 € in the tooltip. An unknown code prints the bare figure. Ignored without money.
emptyMessageempty-messagestringstring | undefinedSentence drawn in place of the matrix when there are no rows, no columns or every value is zero. Defaults to "Nothing to plot yet."; the frame and its heading stay. The chart filters nothing, so this is its only empty string.
headingheadingstringstring | undefinedChart heading (the reference title prop, renamed because title is a native attribute). With what the chart shows, it names the plot for a reader; an aria-label or aria-labelledby on the host names the plot instead, and this summary then describes it.
hideMaxhide-maxbooleanfalseDrop the ring round the largest cell and the note under the scale, for a matrix where no single cell is the story. Shown, the ring and the note paint --sui-chart-heatmap-peak, the accent by default; set it on the element or any ancestor for a color of your own. Any CSS color or var(--token) works; use a token so the color follows the theme. The note is text, so pick a color that reads at 4.5:1 on the card. Reflected.
hoverIndexhover-indexnumber-1Hovered cell, counted row by row (row * cols.length + col); -1 when idle. The pointer and the arrow keys set it and the chart rings the cell and shows its figure. A write from outside does the same and emits nothing, which is how sui-chart-synced mirrors a sibling. The card reads the cell's current figure, so it follows a data change under the pointer.
loadingloadingbooleanfalseThe figures are still on their way: a shimmer stands in for the matrix, the frame and its heading stay, and the host is aria-busy. Reflected, so a page can style the wait.
moneymoneybooleanfalseFormat values as money in currency: compact ($19k) in cells and the scale, exact in the tooltip. Off, the figures print plainly, which is what a count wants; set it for a balance. Reflected.
rowsproperty onlystring[][]Row labels (left, right-aligned, ellipsized at 96px). Each becomes a rowheader in the role="grid" matrix, so a screen reader can walk the figures cell by cell. Property, not attribute.
subtitlesubtitlestringstring | undefinedSecondary line under the heading.
valuesproperty onlynumber[][][]Matrix values[row][col]. Each cell's color mixes the scale's high end into its low end by its share of the largest value, and the scale bar under the matrix shows the same two ends. The high end paints --sui-chart-heatmap-high, --viz-1 by default, and the low end paints --sui-chart-heatmap-low, --surface by default. Set them on the element or any ancestor. Any CSS color or var(--token) works; use a token so the color follows the theme. Property, not attribute.

Events

EventDetailReactVueDescription
suiHoverSuiChartHeatmapHoverDetailonSuiHover@sui-hoverFires when the pointer or the keyboard moves the hover to another cell: { index, row, col }, all -1 when the pointer leaves the matrix, or Escape or a blur takes the keyboard's cell away.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves focus to the matrix, the chart's one tab stop, from which the arrow keys reach the cells. Does nothing while there is nothing to plot.

Slots

SlotDescription
header-endControls at the end of the head row, after the heading.

Types

SuiChartHeatmapHoverDetail interfaceDetail of the heatmap's suiHover. index is the hovered cell counted row by row, the value hoverIndex takes; row and col locate the same cell in rows and cols. All three are -1 when nothing is hovered.
indexnumber
rownumber
colnumber

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also