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
Live · keyboard ready
Electricity generation, 2025. Solar 16%, Wind 22%, Gas 23%, Coal 27%, Hydro 12%.
| Category | Value | Share | Cells |
|---|---|---|---|
| Solar | 350 TWh | 16% | 16 |
| Wind | 490 TWh | 22% | 22 |
| Gas | 520 TWh | 23% | 23 |
| Coal | 610 TWh | 27% | 27 |
| Hydro | 260 TWh | 12% | 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
Live · keyboard ready
Electricity generation, 2025. Solar 16%, Wind 22%, Gas 23%, Coal 27%, Hydro 12%.
| Category | Value | Share | Cells |
|---|---|---|---|
| Solar | 350 TWh | 16% | 16 |
| Wind | 490 TWh | 22% | 22 |
| Gas | 520 TWh | 23% | 23 |
| Coal | 610 TWh | 27% | 27 |
| Hydro | 260 TWh | 12% | 12 |
energy-mix-parts-of-a-whole-as-a-grid-of-squares-easier-to-read-than-slices-at-small-sizes-.tsxtsx
"use client";
import { WaffleChart } from "@/components/manicat/waffle-chart/waffle-chart";
import type { WaffleCategory } from "@/components/manicat/waffle-chart/waffle-chart";
const generation: 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 WaffleChartEnergyExample() {
return <WaffleChart data={generation} label="Electricity generation, 2025" unit="TWh" legend />;
}
Keyboard interactions
| Keys | Action |
|---|---|
| Tab | Focuses the grid, then each legend item. |
| Arrow keys | Move the reading cell by cell across the grid. |
| PageUp / PageDown | Jump to the first cell of the previous or next category. |
| Home / End | Go to the first or last filled cell. |
| Escape | Clears the reading. |
| Enter / Space on a legend item | Pins 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.