Heatmap
A 2D intensity grid (the activity-calendar / punch-card shape), colored on a theme-derived ramp, with a per-cell readout and opt-in clickable cells.
Live demo
Heatmap renders a matrix of values as a grid of colored cells, each cell's fill scaled by its value on an intensity ramp resolved off the live theme, the activity-calendar / punch-card shape (runs per hour over a week, load by host and resource, a GitHub-style contribution grid). Feed it a dense values matrix (row-major) plus optional rows and cols labels; the default intensity ramp washes from a faint surface up to the accent, and thermal or severity (or an explicit color array) swap the scale.
Set categorical to color by category instead of intensity: each column (or each row, with categoryAxis="row") takes a distinct color sampled off any Palette (accents / skittles / statuses), and the cell value washes that hue from the surface up. One grid, then, reads its categories by hue and their magnitude by fill: the categorical heat-grid. Turn scale on and a legend keys the hues. Every cell's intensity is normalized against the data's own maximum, or an explicit max so several grids share one scale. It's interactive: hovering or focusing a cell floats a value readout, and the whole grid is mirrored into a visually-hidden data table so assistive tech reads the numbers, not the pixels. Set selectable to make cells a drill-in surface, each firing a select event with its row, column, and value. showValues prints the number in each cell. Feed an optional second glow matrix (the same shape as values) to carry a second signal in one grid: the fill reads one metric while a per-cell halo reads another (run count vs total runtime, requests vs error rate). An empty matrix shows a muted No data message in place of the grid.
When to use
How this component composes with the rest of the set.
Props
20 props, straight from the manifest.
| Prop | Type | Default | Bindings | Description |
|---|---|---|---|---|
Appearance
States
cell-hover
Pointer over or keyboard focus on a cell: its border brightens (the rest are never dimmed, so a dense grid never strobes) and a value readout floats above it.
cell-focus
Keyboard focus on a cell draws a token ring; each cell is a tab stop that announces its row, column, and value.
current
A cell flagged via current: a thicker accent (or toned) ring marks it as "now", pulsing when currentPulse is set (and reduced-motion is not requested).
Anatomy
The named parts that make up the component, with their selectors.
chart
The <figure> root holding the SVG grid, tooltip, and the accessible data table.
cell
A single grid cell, filled by its value's intensity color; the focus/hover target.
current-cell
A cell marked as the current / "now" position: ringed in the accent (or a chosen tone), optionally pulsing.
tooltip
The floating value readout shown on hover or focus of a cell; it holds one row per cell and reveals the one under the cursor.
tooltip-row
One cell's readout line: its row/column name, its value, and the glow channel's value when there is one (or the cell's own title in place of all three). The fill draws every row up front and the element only unhides the hovered one, so a mod's reshaped row (a swatch, a unit, its own order) survives the hover.
scale
The optional color-scale key below the grid: five ramp swatches between the low and high value endpoints.
legend
The categorical key below the grid: one swatch per category hue, labeled off the category axis.
Tokens & coverage
What the component consumes, checked live against what the algorithm produces.
Live coverage check against the xtyle-default register
(derive(xtyleDefault, { anchors }) →
coverComponent(manifest, register)). Every token this component
consumes must be a key the algorithm produces.
--accent
--bg-0
--border-thick
--border-thin
--danger
--duration-fast
--ease-standard
--elevation-3
--fg-0
--fg-2
--fg-3
--font-sans
--info
--leading-normal
--neutral
--radius-md
--ring
--space-1
--space-2
--space-3
--success
--surface-overlay
--surface-overlay-border
--text-sm
--text-xs
--warn
--weight-semibold
Accessibility
Code
Activity grid
A week-by-hour activity grid on the default intensity ramp, the contribution-graph shape.
<xtyle-heatmap
rows='["Mon","Tue","Wed","Thu","Fri","Sat","Sun"]'
scale
label="Activity by day and hour"></xtyle-heatmap>
<script>
const grid = document.querySelector("xtyle-heatmap");
grid.cols = Array.from({ length: 24 }, (_, h) => String(h));
grid.values = weekByHour; // number[7][24]
</script>
<script lang="ts">
import { Heatmap } from "@xtyle/svelte";
const rows = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const cols = Array.from({ length: 24 }, (_, h) => String(h));
const values = weekByHour; // number[7][24]
</script>
<Heatmap {rows} {cols} {values} scale label="Activity by day and hour" />
---
import { Heatmap } from "@xtyle/astro";
const rows = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const cols = Array.from({ length: 24 }, (_, h) => String(h));
const values = weekByHour; // number[7][24]
---
<Heatmap rows={rows} cols={cols} values={values} scale label="Activity by day and hour" />
Thermal scale with values
scheme="thermal" runs the cells cool to hot, and showValues prints each number in its cell.
<xtyle-heatmap
scheme="thermal"
show-values
label="Load by host and resource"></xtyle-heatmap>
<script>
const grid = document.querySelector("xtyle-heatmap");
grid.rows = ["web-1", "web-2", "db-1"];
grid.cols = ["CPU", "Mem", "Net", "Disk"];
grid.values = [[42, 30, 18, 9], [26, 55, 22, 14], [12, 40, 8, 61]];
</script>
<script lang="ts">
import { Heatmap } from "@xtyle/svelte";
const rows = ["web-1", "web-2", "db-1"];
const cols = ["CPU", "Mem", "Net", "Disk"];
const values = [[42, 30, 18, 9], [26, 55, 22, 14], [12, 40, 8, 61]];
</script>
<Heatmap {rows} {cols} {values} scheme="thermal" showValues label="Load by host and resource" />
---
import { Heatmap } from "@xtyle/astro";
const rows = ["web-1", "web-2", "db-1"];
const cols = ["CPU", "Mem", "Net", "Disk"];
const values = [[42, 30, 18, 9], [26, 55, 22, 14], [12, 40, 8, 61]];
---
<Heatmap rows={rows} cols={cols} values={values} scheme="thermal" showValues label="Load by host and resource" />
Selectable cells
selectable makes each cell a button that fires select on click or Enter/Space, so a grid can drive a drill-in. The detail carries the row, column, value, and both indices.
<xtyle-heatmap
scheme="intensity"
selectable
label="Runs per hour"></xtyle-heatmap>
<script>
const grid = document.querySelector("xtyle-heatmap");
grid.rows = ["Mon", "Tue", "Wed"];
grid.cols = ["AM", "PM", "Night"];
grid.values = [[3, 8, 1], [5, 2, 0], [0, 6, 4]];
grid.addEventListener("select", (e) => {
// { row, col, value, rowIndex, colIndex }
openRuns(e.detail.row, e.detail.col);
});
</script>
<script lang="ts">
import { Heatmap } from "@xtyle/svelte";
const rows = ["Mon", "Tue", "Wed"];
const cols = ["AM", "PM", "Night"];
const values = [[3, 8, 1], [5, 2, 0], [0, 6, 4]];
</script>
<Heatmap
{rows}
{cols}
{values}
scheme="intensity"
selectable
label="Runs per hour"
onselect={(e) => openRuns(e.detail.row, e.detail.col)}
/>
---
import { Heatmap } from "@xtyle/astro";
const rows = ["Mon", "Tue", "Wed"];
const cols = ["AM", "PM", "Night"];
const values = [[3, 8, 1], [5, 2, 0], [0, 6, 4]];
---
<Heatmap rows={rows} cols={cols} values={values} scheme="intensity" selectable label="Runs per hour" />
<script>
document.querySelector("xtyle-heatmap").addEventListener("select", (e) => {
openRuns(e.detail.row, e.detail.col);
});
</script>
Two-channel glow
A second glow matrix carries a second metric as a per-cell halo, so the fill reads run count while the glow reads total runtime, two signals in one grid.
<xtyle-heatmap
scheme="intensity"
glow-label="runtime"
label="Runs (fill) and total runtime (glow) by day and bucket"></xtyle-heatmap>
<script>
const grid = document.querySelector("xtyle-heatmap");
grid.rows = ["Mon", "Tue", "Wed"];
grid.cols = ["AM", "PM", "Night"];
grid.values = [[3, 8, 1], [5, 2, 0], [0, 6, 4]];
grid.glow = [[40, 12, 90], [8, 30, 0], [0, 15, 70]];
</script>
<script lang="ts">
import { Heatmap } from "@xtyle/svelte";
const rows = ["Mon", "Tue", "Wed"];
const cols = ["AM", "PM", "Night"];
const values = [[3, 8, 1], [5, 2, 0], [0, 6, 4]];
const glow = [[40, 12, 90], [8, 30, 0], [0, 15, 70]];
</script>
<Heatmap {rows} {cols} {values} {glow} glowLabel="runtime" scheme="intensity" label="Runs (fill) and total runtime (glow)" />
---
import { Heatmap } from "@xtyle/astro";
const rows = ["Mon", "Tue", "Wed"];
const cols = ["AM", "PM", "Night"];
const values = [[3, 8, 1], [5, 2, 0], [0, 6, 4]];
const glow = [[40, 12, 90], [8, 30, 0], [0, 15, 70]];
---
<Heatmap rows={rows} cols={cols} values={values} glow={glow} glowLabel="runtime" scheme="intensity" label="Runs (fill) and total runtime (glow)" />
Current-cell marker and per-cell titles
current rings the live cell (with currentPulse to breathe it and currentTone to tint it), the "you are here" on a time grid, and titles gives a cell a fuller hover readout that doubles as its accessible name.
<xtyle-heatmap
rows='["Mon","Tue","Wed"]'
current-pulse
label="Runs per bucket, this week"></xtyle-heatmap>
<script>
const grid = document.querySelector("xtyle-heatmap");
grid.cols = ["AM", "PM", "Night"];
grid.values = [[3, 8, 1], [5, 2, 0], [0, 6, 4]];
// ring + pulse the live cell (Wed / PM), and give it a fuller hover readout
grid.current = [[2, 1]];
grid.titles = [[], [], [, "Wed PM: 6 runs · 2 failed · click to view"]];
</script>
<script lang="ts">
import { Heatmap } from "@xtyle/svelte";
const rows = ["Mon", "Tue", "Wed"];
const cols = ["AM", "PM", "Night"];
const values = [[3, 8, 1], [5, 2, 0], [0, 6, 4]];
const current = [[2, 1]];
const titles = [[], [], [, "Wed PM: 6 runs · 2 failed · click to view"]];
</script>
<Heatmap {rows} {cols} {values} {current} {titles} currentPulse label="Runs per bucket, this week" />
---
import { Heatmap } from "@xtyle/astro";
const rows = ["Mon", "Tue", "Wed"];
const cols = ["AM", "PM", "Night"];
const values = [[3, 8, 1], [5, 2, 0], [0, 6, 4]];
const current = [[2, 1]];
const titles = [[], [], [, "Wed PM: 6 runs · 2 failed · click to view"]];
---
<Heatmap rows={rows} cols={cols} values={values} current={current} titles={titles} currentPulse label="Runs per bucket, this week" />
Categorical heat-grid
categorical colors each column a distinct hue sampled off a Palette (skittles here) and washes it up by value, so a grid whose columns are unrelated categories reads by hue, not one shared ramp. scale keys the hues in a legend.
<xtyle-heatmap
scheme="skittles"
categorical
scale
label="Weekly totals by category"></xtyle-heatmap>
<script>
const grid = document.querySelector("xtyle-heatmap");
grid.rows = ["Week 1", "Week 2", "Week 3"];
grid.cols = ["Bugs", "Features", "Docs", "Chores"];
grid.values = [[8, 3, 1, 5], [2, 6, 4, 3], [5, 1, 7, 2]];
</script>
<script lang="ts">
import { Heatmap } from "@xtyle/svelte";
const rows = ["Week 1", "Week 2", "Week 3"];
const cols = ["Bugs", "Features", "Docs", "Chores"];
const values = [[8, 3, 1, 5], [2, 6, 4, 3], [5, 1, 7, 2]];
</script>
<Heatmap {rows} {cols} {values} scheme="skittles" categorical scale label="Weekly totals by category" />
---
import { Heatmap } from "@xtyle/astro";
const rows = ["Week 1", "Week 2", "Week 3"];
const cols = ["Bugs", "Features", "Docs", "Chores"];
const values = [[8, 3, 1, 5], [2, 6, 4, 3], [5, 1, 7, 2]];
---
<Heatmap rows={rows} cols={cols} values={values} scheme="skittles" categorical scale label="Weekly totals by category" />