DHxWhy

Activity Heatmap Calendar

Component 55

About

Activity heatmap calendar for React and Tailwind CSS (shadcn/ui) where every day is a square that flies into weekday and month bar charts. It is built to turn a year of check-ins into answers people can see: the same squares rearrange from the calendar into weekday rows and monthly columns, so the busiest day, the strongest month and the quiet season stand out without reading numbers. Shows total, active days, longest and current streaks, highlights the leading weekday or month, and fits the whole year to any width without horizontal scrolling. Tooltips, keyboard navigation for views and days, controlled or uncontrolled view, optional auto-cycling for showcases, theme tokens for light and dark, reduced-motion support and SSR-safe dates. Use it for habit and fitness trackers, learning streaks, contribution graphs, and support or sales dashboards.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Activity Heatmap Calendar to your project and fits it to your theme.

Code

How Activity Heatmap Calendar is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client";

import * as React from "react";
import { ActivityHeatmap } from "@/components/ui/activity-heatmap";
import type { ActivityDatum, ActivityHeatmapView } from "@/components/ui/activity-heatmap";

export const HEATMAP_DEMO_END = "2025-12-31";
const LIVE_STREAK_DAYS = 6;

// A repeatable year of movement practice. Weekdays are busier; summer is quieter.
export function makeDemoActivity(days = 365): ActivityDatum[] {
  let seed = 20251009;
  const random = () => {
    seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
    return seed / 4294967296;
  };
  const count = Number.isFinite(days) ? Math.max(1, Math.floor(days)) : 365;
  const out: ActivityDatum[] = [];
  for (let index = count - 1; index >= 0; index -= 1) {
    const date = new Date(2025, 11, 31 - index);
    const day = date.getDay();
    const summer = date.getMonth() >= 5 && date.getMonth() <= 7;
    const chance = (day === 2 ? 0.97 : day === 0 || day === 6 ? 0.26 : 0.85) * (summer ? 0.52 : 1);
    const active = index < LIVE_STREAK_DAYS || random() < chance;
    const value = active ? Math.round((20 + random() * 55) * (day === 2 ? 1.25 : 1)) : 0;
    out.push({ date: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`, value });
  }
  return out;
}

export const HEATMAP_DEMO_DATA = makeDemoActivity();
const CCGATHER_URL = "https://ccgather.com/?utm_source=21st&utm_medium=component&utm_campaign=activity_heatmap";

type DemoProps = {
  days?: number;
  cellSize?: number;
  color?: string;
  autoPlay?: boolean;
  interval?: number;
  defaultView?: ActivityHeatmapView;
  bare?: boolean;
  controlled?: boolean;
  empty?: boolean;
};

export default function Demo({ days = 365, cellSize = 10, color, autoPlay, interval, defaultView = "calendar", bare = false, controlled = false, empty = false }: DemoProps) {
  const data = React.useMemo(() => empty ? [] : makeDemoActivity(days), [days, empty]);
  const [selected, setSelected] = React.useState(defaultView);
  const heatmap = <ActivityHeatmap
    data={data}
    days={days}
    endDate={HEATMAP_DEMO_END}
    cellSize={cellSize}
    color={color ?? (bare ? undefined : "var(--color-chart-2, var(--heatmap-demo-accent))")}
    unit="min"
    autoPlay={autoPlay ?? (bare ? undefined : true)}
    interval={interval ?? (bare ? undefined : 1600)}
    defaultView={defaultView}
    {...(controlled ? { view: selected, onViewChange: setSelected } : {})}
  />;
  if (bare) return <div className="flex min-h-screen w-full items-center justify-center bg-background p-6"><div data-controlled-view={controlled ? selected : undefined} className="w-full max-w-3xl">{heatmap}</div></div>;
  return (
    <div data-heatmap-demo className="flex min-h-screen w-full flex-col items-center justify-center gap-4 bg-background p-6 font-sans text-foreground [--heatmap-demo-accent:oklch(0.6_0.12_165)] dark:[--heatmap-demo-accent:oklch(0.72_0.15_163)]">
      <div className="w-full max-w-3xl rounded-2xl border border-border bg-card p-5 text-card-foreground shadow-sm">
        <div className="mb-5 flex items-start justify-between gap-3">
          <div>
            <p className="text-[10px] font-medium uppercase tracking-[0.16em] text-muted-foreground">Movement practice · 2025</p>
            <h2 className="mt-1.5 text-xl font-semibold leading-7 tracking-tight">Make the pattern visible.</h2>
          </div>
          <span className="mt-1 hidden items-center gap-1.5 rounded-full border border-border px-2.5 py-1 text-[10px] text-muted-foreground min-[480px]:inline-flex"><svg aria-hidden="true" width="12" height="12" viewBox="0 0 12 12" fill="currentColor"><path d="M1 7h2v4H1zm4-6h2v10H5zm4 3h2v7H9z" /></svg>Daily practice</span>
        </div>
        {heatmap}
      </div>
      <a href={CCGATHER_URL} target="_blank" rel="noreferrer" className="text-[10px] text-muted-foreground underline underline-offset-4 hover:text-foreground">Made by the CCgather team</a>
    </div>
  );
}

More by DHxWhy

View all
  • Sun Moon Dark Mode Theme Toggle

    Sun Moon Dark Mode Theme Toggle

    186
  • Country Flags Globe

    Country Flags Globe

    110
  • Live Notification Feed

    Live Notification Feed

    109
  • Airplane Window Gallery

    Airplane Window Gallery

    95
  • Domino Scroll Image Gallery

    Domino Scroll Image Gallery

    93
  • QR Code Scan Login Form with Password Fallback

    QR Code Scan Login Form with Password Fallback

    82
  • Constellation Image Gallery

    Constellation Image Gallery

    80
  • Ferris Wheel Image Gallery

    Ferris Wheel Image Gallery

    73