Waffle chart

A ten by ten unit chart where every cell is one percent, and cells fly to their new group when the data changes.

pnpm dlx shadcn@latest add @uiarc/waffle-chart
Live · keyboard ready

Electricity generation, 2023

Germany, 507 TWh

Electricity generation by source, Germany, 2023. Wind and solar 43%, Hydro 4%, Nuclear 1%, Gas 15%, Coal 26%, Other 11%.

Electricity generation by source, Germany, 2023
CategoryValueShareCells
Wind and solar218 TWh43%43
Hydro20 TWh4%4
Nuclear5 TWh1%1
Gas76 TWh15%15
Coal132 TWh26%26
Other56 TWh11%11
  • Showing shares of one whole where people should be able to count units.
  • Comparing the same categories across a few datasets, such as countries or years.
  • Replacing a pie or donut when small shares need to stay visible.
  • Use line-chart or streamgraph for how shares change over time.
  • Use bar-chart when exact comparison of many categories matters more than the whole.
  • Use sunburst for nested parts of a whole.

Installation

Add Waffle chart with the shadcn CLI, or copy the source by hand.

pnpm dlx shadcn@latest add @uiarc/waffle-chart

Adds the component and its local dependencies, and installs motion. First time? Add the @uiarc registry to components.json, or use the full URL:

example.tsx
import { WaffleChart } from "@/registry/components/waffle-chart/waffle-chart"; const mix = [  { key: "wind-solar", label: "Wind and solar", value: 218 },  { key: "hydro", label: "Hydro", value: 20 },  { key: "gas", label: "Gas", value: 76 },  { key: "coal", label: "Coal", value: 132 },  { key: "other", label: "Other", value: 61 },]; export function PowerMix() {  return <WaffleChart data={mix} label="Electricity generation, 2023" unit="TWh" />;}

WaffleChart

A unit chart where every cell is one share of the whole. When the data changes, cells keep following their category and fly to their new block with a staggered spring, and the shares in the legend roll to their new values.

PropTypeDefaultDescription
dataRequiredWaffleCategory[]–Categories in fill order: { key, label, value, color? }. The first fills from the bottom-left corner, column by column. Shares come from the total.
labelRequiredstring–What the whole is, such as "Electricity generation, 2023". Names the chart, the summary, and the table.
unitstring""Unit after raw values in the tooltip and table, such as "TWh".
formatValue(value: number, category: WaffleCategory) => string–Formats raw values. Shares are always percentages.
rowsnumber10Grid rows. rows × columns cells make the whole.
columnsnumber10Grid columns. 10 × 10 means one cell per percent.
accentKeystring | nullfirst keyThe category painted in the first series hue, derived from the accent. The next three take the supporting series and later ones fall back to neutral steps.
activeKeystring | null–Controlled focused category. Other cells dim while one is focused.
defaultActiveKeystring | nullnullInitial focused category when uncontrolled.
onActiveChange(key: string | null) => void–Called when a legend item is pinned or released.
legendbooleantrueCategory list with rolling shares. It sits beside the grid from 420px and below it on narrower containers.
decimalsnumber0Decimal places for shares.
emptyLabelstring"No data"Message when the data is empty or sums to zero.
refRef<HTMLElement>–Forwarded to the figure.
classNamestring–Extra class on the figure.
Tab
Focuses the grid, then each legend item.
Arrow keys
Move the reading cell by cell across the grid.
PageUporPageDown
Jump to the first cell of the previous or next category.
HomeorEnd
Go to the first or last filled cell.
Escape
Clears the reading.
EnterorSpace on a legend item
Pins or releases that category.
  • The grid is a focusable group with a roledescription and instructions; cells are decorative and aria-hidden.
  • A polite live region reads the category, share, and value under the keyboard cursor.
  • A visually hidden summary and a table list every category with its value, share, and cell count.
  • Legend items are toggle buttons with aria-pressed; identity never relies on colour alone because every category is named in the legend.
  • Cells are persistent: a data change keeps as many cells in place as possible and flies the rest to their new block, staggered by position so the grid refills like a wave.
  • A travelling cell dips in scale mid-flight and changes colour with the same delay, so it reads as lifting out of one group and landing in another.
  • Shares in the legend roll to their new values on a critically damped spring.
  • The first time the chart is seen, cells pop in from the bottom-left corner.
  • Reduced motion places cells and shares immediately and drops the dip, pop, and stagger.
  • The grid is square and fills up to 300px; the legend moves beside it from 420px of container width.
  • Legend labels truncate with an ellipsis; the full name stays in the tooltip, the live region, and the table.
  • Touch reads cells with a press and drag; the page still scrolls vertically.
  • One element per cell, 100 by default, animated with transforms only.
  • Hover is resolved from pointer position arithmetic, not per-cell listeners.
  • Grid size is measured with one ResizeObserver.

Notes for AI

Give your coding assistant the Markdown reference instead of screenshots.

  • Choose it for part-to-whole with four to eight categories where countable units help, such as energy mix, survey answers, or budget split.
  • Keep categories in a meaningful order; the fill order is the reading order.
  • Use accentKey for the category the story is about and leave the rest neutral.
  • Switching datasets with the same keys is where it shines: cells travel instead of repainting.

The full library index for assistants is at /llms.txt.