Bankk roll

Contribution Graph

Component 3

About

A composable, customizable contribution graph component inspired by GitHub's activity heatmap. Features composable subcomponents (Grid, Tooltip, Legend), CSS variable theming, configurable cell sizes, interactive tooltips with custom render functions, and full dark mode support.

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

Code

How Contribution Graph is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

// Custom tooltip demo

import {
  ContributionGraph,
  ContributionGraphGrid,
  ContributionGraphTooltip,
  ContributionGraphLegend,
  type ContributionData,
} from "@/components/ui/contribution-graph";

function generateSampleData(): ContributionData[] {
  const data: ContributionData[] = [];
  const endDate = new Date();
  const startDate = new Date();
  startDate.setFullYear(startDate.getFullYear() - 1);

  const current = new Date(startDate);
  while (current <= endDate) {
    const dayOfWeek = current.getDay();
    const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
    const baseChance = isWeekend ? 0.3 : 0.6;

    if (Math.random() < baseChance) {
      data.push({
        date: current.toISOString().split("T")[0],
        count: Math.floor(Math.random() * 12) + 1,
      });
    }
    current.setDate(current.getDate() + 1);
  }
  return data;
}

const sampleData = generateSampleData();
const endDate = new Date();
const startDate = new Date();
startDate.setFullYear(startDate.getFullYear() - 1);
startDate.setDate(startDate.getDate() + 1);

// Calculate stats for tooltip
const totalContributions = sampleData.reduce((sum, d) => sum + d.count, 0);
const avgPerDay = (totalContributions / 365).toFixed(1);

function getActivityLevel(count: number): { label: string; color: string } {
  if (count === 0) return { label: "No activity", color: "text-muted-foreground" };
  if (count <= 2) return { label: "Light", color: "text-blue-500" };
  if (count <= 5) return { label: "Moderate", color: "text-green-500" };
  if (count <= 8) return { label: "Active", color: "text-orange-500" };
  return { label: "Very Active", color: "text-red-500" };
}

function getRelativeDate(dateStr: string): string {
  const date = new Date(dateStr);
  const now = new Date();
  const diffDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));

  if (diffDays === 0) return "Today";
  if (diffDays === 1) return "Yesterday";
  if (diffDays < 7) return `${diffDays} days ago`;
  if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`;
  if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`;
  return `${Math.floor(diffDays / 365)} years ago`;
}

export default function DemoCustomTooltip() {
  return (
    <div className="w-full max-w-4xl p-6">
      <ContributionGraph
        data={sampleData}
        startDate={startDate.toISOString().split("T")[0]}
        endDate={endDate.toISOString().split("T")[0]}
      >
        <ContributionGraphGrid />
        <ContributionGraphTooltip className="min-w-[180px] p-0 overflow-hidden">
          {({ date, count }) => {
            const activity = getActivityLevel(count);
            const dayOfWeek = new Date(date).toLocaleDateString("en-US", { weekday: "long" });

            return (
              <div className="divide-y divide-border">
                {/* Header */}
                <div className="px-3 py-2 bg-muted/50">
                  <div className="flex items-center justify-between gap-3">
                    <span className="text-[10px] uppercase tracking-wider text-muted-foreground">
                      {dayOfWeek}
                    </span>
                    <span className={`text-[10px] font-medium ${activity.color}`}>
                      {activity.label}
                    </span>
                  </div>
                  <div className="text-xs text-muted-foreground mt-0.5">
                    {getRelativeDate(date)}
                  </div>
                </div>

                {/* Stats */}
                <div className="px-3 py-2 space-y-1.5">
                  <div className="flex items-baseline justify-between">
                    <span className="text-2xl font-bold text-popover-foreground tabular-nums">
                      {count}
                    </span>
                    <span className="text-[10px] text-muted-foreground">
                      contribution{count !== 1 ? "s" : ""}
                    </span>
                  </div>

                  {/* Progress bar */}
                  <div className="space-y-1">
                    <div className="h-1.5 w-full bg-muted rounded-full overflow-hidden">
                      <div
                        className="h-full bg-primary transition-all duration-300"
                        style={{ width: `${Math.min(100, (count / 12) * 100)}%` }}
                      />
                    </div>
                    <div className="flex justify-between text-[9px] text-muted-foreground">
                      <span>0</span>
                      <span>Daily avg: {avgPerDay}</span>
                      <span>12+</span>
                    </div>
                  </div>
                </div>

                {/* Footer */}
                <div className="px-3 py-1.5 bg-muted/30">
                  <div className="text-[10px] text-muted-foreground">
                    {new Date(date).toLocaleDateString("en-US", {
                      month: "long",
                      day: "numeric",
                      year: "numeric",
                    })}
                  </div>
                </div>
              </div>
            );
          }}
        </ContributionGraphTooltip>
        <ContributionGraphLegend className="mt-3" />
      </ContributionGraph>
    </div>
  );
}

More by Bankk roll

View all
  • Animated Testimonials

    Animated Testimonials

    1.1K
  • Commerce Hero

    Commerce Hero

    650
  • Acme Hero

    Acme Hero

    437
  • Myna Hero

    Myna Hero

    383
  • Single Pricing Card

    Single Pricing Card

    305
  • Pricing Card

    Pricing Card

    287
  • File Viewer

    File Viewer

    175
  • Code Editor Sheet

    Code Editor Sheet

    142