Skip to content
WANCRAFT

G.180Data & Feedback

Waffle Chart

A grid of cells filled in category order to show parts of a whole, with a staggered fill, highlightable categories and smooth reassignment.

NewReactCSSTailwindintermediate

Overview

Waffle Chart turns proportions into countable cells. Values are allocated to a rows × columns grid with the largest-remainder method, so the counts always add up, and cells fill in category order across rows or down columns; with a total above the sum, the rest stays as empty cells. Cells grow in with a stagger along the fill order, and when values change they re-colour in the same wave, so reassignment reads as a sweep rather than a jump. Hover a cell, or hover or focus a legend item, to highlight that category with its value, share and cell count; click a legend item or tap a cell to pin the highlight. Cells can be squares, rounded squares, circles or anything you render (an icon, for example). The grid is role="img" with a computed label, and a visually hidden list repeats every value.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/waffle-chart.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/waffle-chart.json

Method 02 · Copy and paste

  1. 1. No extra dependencies — React and Tailwind CSS only.
  2. 2. Copy the source from the Code tab into components/ui/waffle-chart.tsx.
  3. 3. Import it and adjust props.
Dependencies
None
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { WaffleChart, type WaffleDatum } from "@/components/ui/waffle-chart";

export function WaffleChartExample() {
  const data: WaffleDatum[] = [
    { label: "Design", value: 42 },
    { label: "Research", value: 22 },
    { label: "Engineering handoff", value: 13 },
    { label: "Meetings", value: 14 },
    { label: "Admin", value: 9 },
  ];

  return (
    <WaffleChart label="Team week, 2026" data={data} format={(v) => `${v}h`} />
  );
}

03Props

PropTypeDefaultDescription
data{ label: string; value: number; color?: string }[]—Categories in fill order (required).
labelstring—Accessible name (required); opens the computed aria-label and the hidden list.
rowsnumber10Grid rows.
columnsnumber10Grid columns.
totalnumber—The whole the grid represents (defaults to the sum); the remainder shows as empty cells.
shape"square" | "rounded" | "circle""rounded"Cell shape (ignored with renderCell).
gapnumber3Gap between cells in px.
cellSizenumber26Largest cell size in px; cells shrink to fit narrow containers.
staggernumber10Delay between consecutive cells in ms (entrance; half of it when values change).
direction"row" | "column""row"Fill across rows from the top, or down columns from the left.
showLegendbooleantrueLegend with values and shares (buttons that highlight and pin).
accentstring"#FF7448"Colour of the first category; others use a categorical palette.
format(value: number) => string—Value formatter for the read-out, legend and labels.
renderCell(cell: WaffleCell) => ReactNode—Custom cell content (e.g. an icon) given { order, category, datum, color, active }.

04Features

  • 01Largest-remainder allocation: cell counts always add up to the grid (or the filled share of a total)
  • 02Staggered entrance along the fill order; value changes re-colour cells in the same wave
  • 03Category highlight from cells or legend with value, share and cell count; click or tap to pin
  • 04Square, rounded, circle or custom cells via a render prop
  • 05Pure CSS animation and transitions — no animation library, no JavaScript loop
motion · staggermotion · tweeninput · hoverinput · focusinput · clickinput · keyboard

05Accessibility

  • The grid is role="img" with a computed aria-label listing every category's value and share.
  • A visually hidden list repeats each category with its value, share and cell count.
  • Legend items are buttons: focus highlights a category, Enter or Space pins it (aria-pressed), Tab moves on.
  • Highlighting never relies on colour alone — the read-out names the category and its numbers.
  • With prefers-reduced-motion cells appear and re-colour instantly.