Heatmap Chart
- Stable
- WCAG 2.2 evidence
- RTL
A heat map: cells coloured by value on the five-step heatmap ramp, with a legend of the ramp and its numeric ranges. Data driven:
rows, columns and values (or days) in, SVG out; the same props render the static SVG on the server. Accessibility: the figure is named by title, described by summary and backed by a data table with every value (WCAG 1.1.1,
1.3.1). The value is never told by colour alone (1.4.1): the legend lists the numeric range of every step, cells can carry the number,
and each cell is announced and shown in the tooltip with its row, column and value. The plot is ONE tab stop in a 2D grid: the
arrow keys move one cell in both directions, Home and End go to the row's ends, Page Up and Page Down to the column's ends, Ctrl
plus Home or End to the first and last cell; the tooltip shows on hover and focus, stays while hovered and closes with Escape
(2.1.1, 1.4.13). The cells are img elements in a group, like the other charts, not an ARIA grid: the heat map is read-only data,
and the data table is the real two-dimensional alternative with row and column headers; an ARIA grid on SVG is supported
unevenly and would promise cell navigation that screen readers do not give. At a narrow width the cells keep a readable size and the plot
scrolls inside a labelled, focusable region (1.4.10). The draw-in is behind motion-safe:; the plot stays left-to-right in
right-to-left pages while the card, legend and table mirror, and digits and month and weekday names follow the locale. Consumer duties: a title that asks the question, a summary with the main message, units in valueFormat, thresholds that mean
something. Contrast: the numbers on cells are 4.5:1 or better on every step except step 3 in the dark theme (4.4:1), and the lightest
steps are under 3:1 against the card, which is why the value is also text.
import { HeatmapChart } from "@nexera-ui/react";- 13
- examples
- 28
- props
- 4
- live controls
- 1
- platform
- 11
- WCAG criteria
- 1
- block uses it
Try every prop. Copy the code.
Change the props and the code updates. Check light and dark, LTR and RTL, and phone width.
import { HeatmapChart } from "@nexera-ui/react";
<HeatmapChart />Examples 12
The same examples as Storybook, rendered live. Open Code to copy one.
Calendar
The calendar layout at a normal card width: cells shrink to 10 px, then the plot scrolls inside a labelled, focusable region (Tab to it, then arrow keys scroll).
import { HeatmapChart } from "@nexera-ui/react";
const calendarDays = (start: string, count: number): { date: string; value: number }[] =>
Array.from({ length: count }, (_, i) => {
const day = new Date(Date.parse(`${start}T00:00:00Z`) + i * 86_400_000);
const weekday = day.getUTCDay();
const wave = Math.round(6 + 5 * Math.sin(i / 17) + ((i * 37) % 7) / 2);
return {
date: day.toISOString().slice(0, 10),
value: weekday === 0 || weekday === 6 ? Math.max(0, wave - 8) : wave,
};
});
<HeatmapChart
title="Check-in activity"
description="Employees checking in per day, last 12 months"
summary="Check-ins follow the working week and dip in the summer."
layout="calendar"
days={calendarDays("2025-10-06", 364)}
valueLabel="Check-ins"
/>;
Calendar three weeks
A short calendar starting on a Sunday: three weeks, the weekday names in the page's language.
import { HeatmapChart } from "@nexera-ui/react";
const calendarDays = (start: string, count: number): { date: string; value: number }[] =>
Array.from({ length: count }, (_, i) => {
const day = new Date(Date.parse(`${start}T00:00:00Z`) + i * 86_400_000);
const weekday = day.getUTCDay();
const wave = Math.round(6 + 5 * Math.sin(i / 17) + ((i * 37) % 7) / 2);
return {
date: day.toISOString().slice(0, 10),
value: weekday === 0 || weekday === 6 ? Math.max(0, wave - 8) : wave,
};
});
<HeatmapChart
title="Leave this month"
description="Employees on leave per day"
summary="Leave peaks in the second week."
layout="calendar"
weekStart={0}
days={calendarDays("2026-09-27", 21)}
valueLabel="On leave"
valueLabels="all"
/>;
Value on every cell
Every cell shows its number (where it fits); the legend still lists the ranges, and the table has every value.
import { HeatmapChart } from "@nexera-ui/react";
<HeatmapChart valueLabels="all" />;
Custom thresholds
Meaningful bands: you set the four values where the ramp steps start, and the legend follows.
import { HeatmapChart } from "@nexera-ui/react";
<HeatmapChart thresholds={[10, 20, 30, 40]} domain={[0, 50]} valueLabels="all" />;
Missing values
Cells with no data are dashed outlines with their own legend entry, never a step of the ramp.
import { HeatmapChart } from "@nexera-ui/react";
const VALUES = [
[2, 4, 18, 34, 27, 11, 5, 3, 1],
[1, 5, 19, 36, 30, 12, 6, 2, 2],
[3, 6, 20, 38, 27, 13, 4, 3, 1],
[2, 4, 12, 30, 32, 19, 5, 2, 3],
[1, 3, 11, 20, 27, 18, 9, 4, 2],
];
<HeatmapChart
values={VALUES.map((row, r) => row.map((value, c) => ((r + c) % 7 === 3 ? null : value)))}
/>;
Single cell
One cell.
import { HeatmapChart } from "@nexera-ui/react";
<HeatmapChart
rows={["Mon"]}
columns={["8:30"]}
values={[[34]]}
summary="34 employees checked in on Monday at 8:30."
/>;
Large grid
A busy grid: seven days by 24 hours. The cells keep a readable width and the plot scrolls inside its region on a narrow card.
import { HeatmapChart } from "@nexera-ui/react";
<HeatmapChart
title="Activity by hour"
description="Requests per hour, last week"
summary="Activity peaks in the early afternoon on weekdays."
rows={["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]}
columns={Array.from({ length: 24 }, (_, i) => `${String(i).padStart(2, "0")}:00`)}
values={Array.from({ length: 7 }, (_, r) =>
Array.from({ length: 24 }, (_, c) =>
r > 4 ? 2 : Math.max(0, Math.round(30 - Math.abs(c - 14) * 4 + ((r * 5 + c * 3) % 6))),
),
)}
valueLabel="Requests"
/>;
Long labels
Long row and column names: row labels widen the gutter, column labels thin out; the cell names, the tooltip and the table keep them whole.
import { HeatmapChart } from "@nexera-ui/react";
<HeatmapChart
rows={["Engineering and platform infrastructure", "Customer success and onboarding", "Finance"]}
columns={["Morning stand-up", "Planning session", "Retrospective", "Demo day"]}
values={[
[12, 4, 9, 20],
[8, 15, 3, 11],
[2, 6, 18, 7],
]}
/>;
Loading
Loading, empty and error are ChartStates inside the card; the header stays.
import { HeatmapChart } from "@nexera-ui/react";
<HeatmapChart state={{ status: "loading" }} />;
Empty
import { HeatmapChart } from "@nexera-ui/react";
<HeatmapChart
rows={[]}
values={[]}
state={{
status: "empty",
title: "Nobody checked in this month",
description: "Try the previous month.",
}}
/>;
Error state
import { Button, HeatmapChart } from "@nexera-ui/react";
<HeatmapChart
state={{
status: "error",
title: "Couldn't load check-ins",
description: "Check your connection and try again.",
action: (
<Button variant="secondary" size="sm">
Try again
</Button>
),
}}
/>;
Right to left
Right-to-left page: the card, legend and table mirror, the grid keeps its left-to-right time axis (ArrowRight is still the next time slot), digits follow the locale.
import { HeatmapChart } from "@nexera-ui/react";
<HeatmapChart
title="أوقات تسجيل الحضور"
description="عدد الموظفين لكل ٣٠ دقيقة هذا الشهر"
summary="يسجّل معظم الموظفين حضورهم بين ٨:٣٠ و٩:٠٠."
rows={["الاثنين", "الثلاثاء", "الأربعاء", "الخميس", "الجمعة"]}
columns={["٧:٠٠", "٧:٣٠", "٨:٠٠", "٨:٣٠", "٩:٠٠", "٩:٣٠", "١٠:٠٠", "١٠:٣٠", "١١:٠٠"]}
valueLabel="تسجيلات الحضور"
lessLabel="أقل"
moreLabel="أكثر"
scrollLabel="أوقات تسجيل الحضور، قابل للتمرير"
table={{ categoryLabel: "اليوم", label: "عرض البيانات كجدول" }}
/>;
Props 28
Press "Try it" on a card to load that prop into the playground.
28 props shown
layoutNexera"grid" | "calendar"The grid layout, the default. The calendar layout.
rowsNexerareadonly string[]The names of the rows, from the top ("Mon", "Tue",...). Row labels sit at the start of the plot.
columnsNexerareadonly string[]The names of the columns, from the start ("7:00", "7:30",...). Labels that do not fit are thinned out.
valuesNexerareadonly (readonly (number | null)[])[]The values: values[row][column]. null, or a missing entry, is no data and draws a dashed outline instead of a step of the ramp.
heightNexeranumberHeight of the plot in px, divided among the rows.
daysNexerareadonly HeatmapDay[]The days, in any order, each with a yyyy-mm-dd date and a value. The calendar runs from the week of the first day to the week of
the last; a day in between that is not listed is no data. A date that is not a real date is skipped.
weekStartNexera0 | 2 | 4 | 1 | 3 | 5 | 6The weekday the rows start with: 0 is Sunday, 1 Monday, 6 Saturday.
valueLabelNexerastringWords for the value in the tooltip and the point labels, and the header of the value column of a calendar's table ("Check-ins", "Hours"). Translate it.
valueLabelsNexera"none" | "high" | "all"Numbers on the cells: high labels the two darkest steps of the ramp, all every
cell, none none. A number is only drawn where it fits the cell, so a calendar of small cells shows none. The value is also in
the tooltip, the point labels and the table, so it is never told by colour alone (WCAG 1.4.1).
domainNexerareadonly [number, number]The lower and upper edge of the ramp. Values at or below the lower edge are step 1, the upper edge is step 5.
thresholdsNexerareadonly [number, number, number, number]The four values where the ramp steps 2 to 5 start, ascending (use them for meaningful bands: 10, 20, 30, 40). The legend shows the ranges they make.
lessLabelNexerastringThe word at the low end of the legend ramp. Translate it.
moreLabelNexerastringThe word at the high end of the legend ramp. Translate it.
scrollLabelNexerastringName of the scroll region that holds the plot when the container is too narrow for the cells. Translate it.
title*NexerastringChart title: the question the chart answers ("Headcount"). Required: it names the figure, the plot and the data table (WCAG 1.1.1).
descriptionNexeraReactNodeSecondary line under the title.
summary*NexerastringOne or two sentences with what the chart shows and its main message ("Headcount grew from 96 to 128 over twelve months"). Required: it describes the figure for assistive technology (WCAG 1.1.1); the data table has the details.
rangeNexeraChartRangePropsRange switch in the header: 3M, 6M, 12M. Pass the data of the chosen range back in.
actionsNexeraReactNodeExtra controls in the header, after the range switch.
kpiNexeraChartKpiPropsKPI row above the plot: the headline number with an optional label or change badge.
stateNexeraChartPlotStateThe state that replaces the plot: loading (skeleton), empty or error. A chart without a value to plot shows the empty state
by itself; pass { status: "empty", title, description, action } to give it your wording.
tableNexeraChartTableOptionsThe data table that is the text alternative: caption, first-column header and whether it is visible or only for assistive technology.
localeNexerastringLocale of digits and separators.
valueFormatNexeraNumberFormatOptionsIntl.NumberFormat options for values ({ style: "currency", currency: "PKR" }, { style: "percent" }). Serialisable, so it
works from a Server Component; formatValue needs a Client Component.
formatValueNexera(value: number) => stringFormats a value for tooltips, point labels and the table (full precision). Wins over valueFormat.
formatAxisValueNexera(value: number) => stringFormats an axis tick (short: "29M").
plotLabelNexerastringAccessible name of the plot group. Translate it.
noDataLabelNexerastringText for a missing value in the tooltip and the table. Translate it.
* Required. React Aria props shown are the ones most apps use; the component accepts the rest of its React Aria props too.
Accessible by default.
Built on React Aria, and covered by the WCAG 2.2 evidence generated on every build.
WCAG 2.2 evidence
8 direct · 3 supporting- 1.1.1Non-text ContentLevel A · tested directly
- 1.3.1Info and RelationshipsLevel A · tested directly
- 1.4.1Use of ColorLevel A · tested directly
- 1.4.3Contrast (Minimum)Level AA · supporting test
- 1.4.10ReflowLevel AA · tested directly
- 1.4.13Content on Hover or FocusLevel AA · tested directly
- 2.1.1KeyboardLevel A · tested directly
- 2.3.3Animation from InteractionsLevel AAA · supporting test
- 2.4.7Focus VisibleLevel AA · supporting test
- 4.1.2Name, Role, ValueLevel A · tested directly
- 4.1.3Status MessagesLevel AA · tested directly
Fits any width.
Nexera components respond to the space they are given. Drag the corner of the frame, or pick a width.
Styling hooks
Pass className to add Tailwind classes (merged last). State is exposed as data attributes, so you can style it with variants like data-pressed:.
<HeatmapChart className="shadow-sm" />Used in blocks
Related components
- AccordionItemOne collapsible section of an FAQ or settings page.
- AreaChartA stacked area chart: layers piled up over time with the title, range switch, actions and legend of the card.
- AvailabilityRowOne person's availability across a window of the day: an avatar with initials and the name, and a timeline with the busy periods drawn as bars.
- AvatarA person or organisation shown as a photo, initials or an icon.
- AvatarGroupA row of overlapping avatars with a "+N" chip for the rest.
- AvatarLabelAn avatar with a name and an optional subtitle beside it, for a person in a list, a header or a mention.
- BadgeShort status or category label: `Tone` x `Variant` x `Size`, with an optional dot, leading icon and remove button.
- BadgeGroupA pill that pairs a small badge with a short message, for an announcement or a status line.