Shadcn Heatmap Generator

Shade a grid of values by intensity, which suits cohort retention tables and activity-by-hour grids. It's built from Tailwind and CSS, with no chart library.

CSV, TSV from Excel/Sheets, a Markdown table, or JSON.

CSV
Values
Code
Week 0
Week 1
Week 2
Week 3
Week 4
Week 5
Jan
100
62
48
41
37
34
Feb
100
65
51
44
40
Mar
100
68
55
47
Apr
100
70
57
May
100
72
Jun
100
34100
Image
chart.tsx
"use client"

const data = [
  { Cohort: "Jan", "Week 0": 100, "Week 1": 62, "Week 2": 48, "Week 3": 41, "Week 4": 37, "Week 5": 34 },
  { Cohort: "Feb", "Week 0": 100, "Week 1": 65, "Week 2": 51, "Week 3": 44, "Week 4": 40, "Week 5": null },
  { Cohort: "Mar", "Week 0": 100, "Week 1": 68, "Week 2": 55, "Week 3": 47, "Week 4": null, "Week 5": null },
  { Cohort: "Apr", "Week 0": 100, "Week 1": 70, "Week 2": 57, "Week 3": null, "Week 4": null, "Week 5": null },
  { Cohort: "May", "Week 0": 100, "Week 1": 72, "Week 2": null, "Week 3": null, "Week 4": null, "Week 5": null },
  { Cohort: "Jun", "Week 0": 100, "Week 1": null, "Week 2": null, "Week 3": null, "Week 4": null, "Week 5": null },
]

const columns = ["Week 0", "Week 1", "Week 2", "Week 3", "Week 4", "Week 5"] as const

/** Cells mix this color into the muted background by value. */
const heatColor = "var(--chart-1)"

function formatValue(value: number) {
  return new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 }).format(value)
}

export function Chart() {
  const values = data.flatMap((row) => columns.map((column) => row[column]))
  const numbers = values.filter((value): value is number => typeof value === "number")
  const min = numbers.length ? Math.min(...numbers) : 0
  const max = numbers.length ? Math.max(...numbers) : 0

  return (
    <div className="flex w-full flex-col gap-4">
      <div className="w-full overflow-x-auto">
        <div
          role="table"
          className="grid min-w-fit gap-1 text-xs"
          style={{ gridTemplateColumns: `auto repeat(${columns.length}, minmax(2.75rem, 1fr))` }}
        >
          <div role="row" className="contents">
            <div role="presentation" />
            {columns.map((column) => (
              <div key={column} role="columnheader" className="truncate px-1 pb-1 text-center font-medium text-muted-foreground">
                {column}
              </div>
            ))}
          </div>
          {data.map((row, r) => (
            <div key={String(row.Cohort)} role="row" className="contents">
              <div role="rowheader" className="flex items-center justify-end pr-2 font-medium whitespace-nowrap text-muted-foreground">
                {String(row.Cohort)}
              </div>
              {columns.map((column, c) => {
                const value = row[column]
                if (typeof value !== "number") {
                  return <div key={column} role="cell" className="h-10 rounded-md bg-muted/40" />
                }
                const intensity = max === min ? 1 : (value - min) / (max - min)
                return (
                  <div
                    key={column}
                    role="cell"
                    title={`${String(row.Cohort)} · ${column}: ${formatValue(value)}`}
                    className="flex h-10 items-center justify-center rounded-md font-medium tabular-nums transition-transform duration-150 hover:z-10 hover:scale-110 hover:shadow-md motion-safe:animate-in motion-safe:fade-in-0 motion-safe:zoom-in-75"
                    style={{
                      backgroundColor: `color-mix(in oklab, ${heatColor} ${Math.round(12 + intensity * 88)}%, var(--muted))`,
                      color: intensity > 0.6 ? "white" : "var(--foreground)",
                      animationDelay: `${(r + c) * 30}ms`,
                      animationFillMode: "both",
                    }}
                  >
                    {formatValue(value)}
                  </div>
                )
              })}
            </div>
          ))}
        </div>
      </div>
      <div className="flex items-center justify-end gap-2 text-xs text-muted-foreground tabular-nums">
        <span>{formatValue(min)}</span>
        <span
          className="h-2 w-32 rounded-full"
          style={{ background: `linear-gradient(to right, color-mix(in oklab, ${heatColor} 12%, var(--muted)), ${heatColor})` }}
        />
        <span>{formatValue(max)}</span>
      </div>
    </div>
  )
}

When to use a heatmap

Heatmaps show patterns across two dimensions at once.

  • Weekly cohort retention (triangle tables)
  • Activity by weekday and hour
  • Correlation or comparison matrices

Data format

  • Column 1 labels the rows; every other column is a column of the grid.
  • Blank cells stay empty, which suits cohort triangles.
  • Choose Percent to show 0–1 values, or values that are already percentages, with a % sign.
  • Paste CSV, JSON from your API, TSV copied from Excel or Google Sheets, or a Markdown table. The generated component keeps your column names as its data keys.
CSV
Cohort,Week 0,Week 1,Week 2,Week 3,Week 4,Week 5
Jan,100,62,48,41,37,34
Feb,100,65,51,44,40,
Mar,100,68,55,47,,
Apr,100,70,57,,,
May,100,72,,,,
Jun,100,,,,,
Same data as JSON
[
  {"cohort": "Jan", "week0": 100, "week1": 62, "week2": 48, "week3": 41, "week4": 37, "week5": 34},
  {"cohort": "Feb", "week0": 100, "week1": 65, "week2": 51, "week3": 44, "week4": 40, "week5": null},
  {"cohort": "Mar", "week0": 100, "week1": 68, "week2": 55, "week3": 47, "week4": null, "week5": null},
  …
]

Options

Values
Show numbers, percentages, or hide the labels.
Heat color
The color cells blend toward at the maximum.

How to add it to your project

  1. 1. This component is plain React and Tailwind: no chart library to install.
  2. 2. Paste your data above, pick the options, and copy the generated chart.tsx.
  3. 3. Save it in your project (e.g. components/heatmap-chart.tsx) and render <Chart />, or <Chart data={rows} /> in Data as prop mode.

Heatmap FAQ