Bloom Logo

Heatmap Grid

A GitHub-like contribution grid/heatmap matrix for activity mapping, with customizable colors, dates, and tooltip details.

importimport { HeatmapGrid } from "@/components/ui/heatmapGrid/heatmapGrid";
$npx @bloomui-react/cli add heatmapGrid
heatmapGrid.tsx
"use client";

import * as React from "react";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip/tooltip";
import { designRadius } from "@/lib/design-system";
import { cn } from "@/lib/utils";

export interface HeatmapValue {
  date: Date | string;
  count: number;
}

export interface HeatmapGridProps {
  className?: string;
  data?: HeatmapValue[];
  startDate?: Date;
  endDate?: Date;
  colorTheme?: "emerald" | "sky" | "indigo" | "rose" | "amber";
  radius?: keyof typeof designRadius;
}

const themeColorMap = {
  emerald: [
    "bg-zinc-100 dark:bg-zinc-800/60",
    "bg-emerald-200 dark:bg-emerald-950/40 text-emerald-800 dark:text-emerald-400",
    "bg-emerald-300 dark:bg-emerald-800/60 text-white",
    "bg-emerald-400 dark:bg-emerald-600 text-white",
    "bg-emerald-500 dark:bg-emerald-400 text-white",
  ],
  sky: [
    "bg-zinc-100 dark:bg-zinc-800/60",
    "bg-sky-200 dark:bg-sky-950/40 text-sky-800 dark:text-sky-400",
    "bg-sky-300 dark:bg-sky-800/60 text-white",
    "bg-sky-400 dark:bg-sky-600 text-white",
    "bg-sky-500 dark:bg-sky-400 text-white",
  ],
  indigo: [
    "bg-zinc-100 dark:bg-zinc-800/60",
    "bg-indigo-200 dark:bg-indigo-950/40 text-indigo-800 dark:text-indigo-400",
    "bg-indigo-300 dark:bg-indigo-800/60 text-white",
    "bg-indigo-400 dark:bg-indigo-600 text-white",
    "bg-indigo-500 dark:bg-indigo-400 text-white",
  ],
  rose: [
    "bg-zinc-100 dark:bg-zinc-800/60",
    "bg-rose-200 dark:bg-rose-950/40 text-rose-800 dark:text-rose-400",
    "bg-rose-300 dark:bg-rose-800/60 text-white",
    "bg-rose-400 dark:bg-rose-600 text-white",
    "bg-rose-500 dark:bg-rose-400 text-white",
  ],
  amber: [
    "bg-zinc-100 dark:bg-zinc-800/60",
    "bg-amber-200 dark:bg-amber-950/40 text-amber-800 dark:text-amber-400",
    "bg-amber-300 dark:bg-amber-800/60 text-white",
    "bg-amber-400 dark:bg-emerald-600 text-white",
    "bg-amber-500 dark:bg-emerald-400 text-white",
  ],
};

export const HeatmapGrid: React.FC<HeatmapGridProps> = ({
  className,
  data = [],
  startDate = new Date(new Date().getFullYear(), 0, 1),
  endDate = new Date(new Date().getFullYear(), 11, 31),
  colorTheme = "emerald",
  radius = "xs",
}) => {
  const [mounted, setMounted] = React.useState(false);

  React.useEffect(() => {
    setMounted(true);
  }, []);

  const getDaysArray = (start: Date, end: Date) => {
    const arr = [];
    const dt = new Date(start);
    while (dt <= end) {
      arr.push(new Date(dt));
      dt.setDate(dt.getDate() + 1);
    }
    return arr;
  };

  const dates = React.useMemo(
    () => getDaysArray(startDate, endDate),
    [startDate, endDate],
  );

  const dataMap = React.useMemo(() => {
    const map = new Map<string, number>();
    for (const val of data) {
      const dateStr =
        val.date instanceof Date
          ? val.date.toISOString().split("T")[0]
          : new Date(val.date).toISOString().split("T")[0];
      map.set(dateStr, val.count);
    }
    return map;
  }, [data]);

  const getIntensityIndex = (count: number) => {
    if (!count) return 0;
    if (count <= 2) return 1;
    if (count <= 5) return 2;
    if (count <= 8) return 3;
    return 4;
  };

  const formatDate = (date: Date) => {
    return date.toLocaleDateString("en-US", {
      month: "short",
      day: "numeric",
      year: "numeric",
    });
  };

  if (!mounted) {
    return (
      <div
        className={cn(
          "w-full h-32 bg-zinc-50 dark:bg-zinc-900/10 animate-pulse rounded-lg border border-dashed border-zinc-200 dark:border-zinc-800",
          className,
        )}
      />
    );
  }

  return (
    <TooltipProvider>
      <div
        className={cn(
          "flex flex-col gap-2 w-full overflow-x-auto p-4 select-none",
          className,
        )}
      >
        <div className="flex gap-[3px] min-w-max">
          {Array.from({ length: 53 }).map((_, colIdx) => {
            const colDates = dates.slice(colIdx * 7, (colIdx + 1) * 7);
            if (colDates.length === 0) return null;

            return (
              <div key={colIdx} className="flex flex-col gap-[3px]">
                {colDates.map((date) => {
                  const dateStr = date.toISOString().split("T")[0];
                  const count = dataMap.get(dateStr) || 0;
                  const intensity = getIntensityIndex(count);
                  const colorClass = themeColorMap[colorTheme][intensity];

                  return (
                    <Tooltip key={dateStr}>
                      <TooltipTrigger asChild>
                        <div
                          className={cn(
                            "size-3 transition-colors cursor-pointer border border-transparent hover:border-zinc-400 dark:hover:border-zinc-500",
                            designRadius[radius],
                            colorClass,
                          )}
                        />
                      </TooltipTrigger>
                      <TooltipContent>
                        {`${count} activities on ${formatDate(date)}`}
                      </TooltipContent>
                    </Tooltip>
                  );
                })}
              </div>
            );
          })}
        </div>
        <div className="flex items-center gap-1.5 text-xs text-zinc-400 dark:text-zinc-500 mt-2 justify-end">
          <span>Less</span>
          {themeColorMap[colorTheme].map((colorClass, idx) => (
            <div
              key={idx}
              className={cn(
                "size-3 border border-transparent",
                designRadius[radius],
                colorClass,
              )}
            />
          ))}
          <span>More</span>
        </div>
      </div>
    </TooltipProvider>
  );
};

HeatmapGrid.displayName = "HeatmapGrid";

Default

Standard activity board showing random sample data mapped across the current calendar year.

data: HeatmapValue[]colorTheme: emerald | sky | indigo | rose | amber

Color Themes

Select different color palettes such as sky, indigo, rose, or amber to fit your design system.

colorTheme: emerald | sky | indigo | rose | amber
Sky Theme
Indigo Theme
Rose Theme
API Reference

Props — HeatmapGrid

Props for the HeatmapGrid component.

PropTypeDefaultDescription
dataHeatmapValue[][]List of data cells containing dates and numeric activity scores.
startDateDateJan 1st (Current Year)Starting date plotted on the board.
endDateDateDec 31st (Current Year)Ending date plotted on the board.
colorTheme'emerald' | 'sky' | 'indigo' | 'rose' | 'amber''emerald'Color preset style applied to intensity tiers.
radiuskeyof typeof designRadius'xs'Corner radius design token of the individual squares.