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.

datachartnew
Preview
Waffle chart preview

Electricity generation, 2025. Solar 16%, Wind 22%, Gas 23%, Coal 27%, Hydro 12%.

Electricity generation, 2025
CategoryValueShareCells
Solar350 TWh16%16
Wind490 TWh22%22
Gas520 TWh23%23
Coal610 TWh27%27
Hydro260 TWh12%12

When to use

  • 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.

When not to use

  • 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.

shadcn CLI

Also installs (1)

  • manicat-foundation
Manual install(+)
registry.jsonjson
{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "waffle-chart",
  "type": "registry:ui",
  "title": "Waffle chart",
  "description": "A ten by ten unit chart where every cell is one percent, and cells fly to their new group when the data changes.",
  "categories": [
    "data"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://manicat.viraza.net/r/manicat-foundation.json"
  ],
  "files": [
    {
      "path": "registry/components/waffle-chart/waffle-chart.tsx",
      "type": "registry:component",
      "target": "@components/manicat/waffle-chart/waffle-chart.tsx"
    },
    {
      "path": "registry/components/waffle-chart/waffle-chart.module.css",
      "type": "registry:component",
      "target": "@components/manicat/waffle-chart/waffle-chart.module.css"
    }
  ],
  "docs": "Docs and live preview: https://uiarc.dev/components/waffle-chart",
  "meta": {
    "tier": "free",
    "kind": "component",
    "docs": "https://manicat.viraza.net/components/waffle-chart",
    "markdown": "https://uiarc.dev/components/waffle-chart/markdown",
    "tags": [
      "data",
      "chart",
      "new"
    ]
  }
}

Usage

Install the component, then copy this into your app. Change the content and the props to fit your screen.

example.tsxtsx
"use client";

import { WaffleChart } from "@/components/manicat/waffle-chart/waffle-chart";
import type { WaffleCategory } from "@/components/manicat/waffle-chart/waffle-chart";

const waffleData: WaffleCategory[] = [
  { key: "solar", label: "Solar", value: 350 },
  { key: "wind", label: "Wind", value: 490 },
  { key: "gas", label: "Gas", value: 520 },
  { key: "coal", label: "Coal", value: 610 },
  { key: "hydro", label: "Hydro", value: 260 },
];

export function WaffleChartExample() {
  // Size containers have no width of their own: give the demo the whole row.
  return <div className="w-full"><WaffleChart data={waffleData} label="Electricity generation, 2025" unit="TWh" legend /></div>;
}

Examples

Real screens rather than isolated props. Switch to Code to copy a whole example into your project.

Energy mix Parts of a whole as a grid of squares, easier to read than slices at small sizes.

Energy mix Parts of a whole as a grid of squares, easier to read than slices at small sizes. preview

Electricity generation, 2025. Solar 16%, Wind 22%, Gas 23%, Coal 27%, Hydro 12%.

Electricity generation, 2025
CategoryValueShareCells
Solar350 TWh16%16
Wind490 TWh22%22
Gas520 TWh23%23
Coal610 TWh27%27
Hydro260 TWh12%12

Keyboard interactions

KeysAction
TabFocuses the grid, then each legend item.
Arrow keysMove the reading cell by cell across the grid.
PageUp / PageDownJump to the first cell of the previous or next category.
Home / EndGo to the first or last filled cell.
EscapeClears the reading.
Enter / Space on a legend itemPins or releases that category.

Accessibility

  • 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.

Motion

  • 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.

Responsive behavior

  • 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.

Performance

  • 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

  • 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.