Contributions

Contribution heatmap card.

Heatmap

A contribution grid.

Contributions this year

958

+14.8%

9B

Lifetime tokens

562.7M

Peak tokens

12h 54m

Longest task

62 days

Top streak

Activity

JanFebMarAprMayJunJulAugSepOctNovDec
function ContributionsDemo() {
  return (
    <ContributionsCard
      stats={CONTRIBUTIONS_STATS}
      headline={CONTRIBUTIONS_HEADLINE}
      delta={CONTRIBUTIONS_DELTA}
    />
  );
}
function ContributionsDemo() {
  return (
    <ContributionsCard
      stats={CONTRIBUTIONS_STATS}
      headline={CONTRIBUTIONS_HEADLINE}
      delta={CONTRIBUTIONS_DELTA}
    />
  );
}

Installation

npx shadcn@latest add https://boardcn.dev/r/contributions-card.json
npx shadcn@latest add https://boardcn.dev/r/contributions-card.json

npm packages

  • react-aria-components

BoardCN dependencies

The CLI installs these for you — you do not need to add them yourself.

Source

The file the CLI copies into your project.

components/charts/contributions-card.tsx
"use client";

import { useState } from "react";
import { Button } from "react-aria-components";
import { Chip } from "@/components/base/badges/chip";
import {
  SegmentedControl,
  SegmentedControlItem,
} from "@/components/base/segmented-control/segmented-control";
import { Tooltip, TooltipTrigger } from "@/components/base/tooltip/tooltip";
import { cx } from "@/utils/cx";

export const ACCENTS = [
  "emerald",
  "green",
  "teal",
  "cyan",
  "blue",
  "indigo",
  "violet",
  "rose",
  "amber",
] as const;

export type ContributionsAccent = (typeof ACCENTS)[number];

export type ContributionsStat = {
  value: string;
  label: string;
};

const MONTHS = [
  "Jan",
  "Feb",
  "Mar",
  "Apr",
  "May",
  "Jun",
  "Jul",
  "Aug",
  "Sep",
  "Oct",
  "Nov",
  "Dec",
] as const;

const CONTRIBUTION_RANGES = [
  [0, 0],
  [1, 4],
  [5, 9],
  [10, 15],
  [16, 24],
  [25, 40],
] as const;

const CURRENT_YEAR = new Date().getFullYear();

const COLUMN_CLASSES: Record<number, string> = {
  37: "grid-cols-[repeat(37,13px)] sm:grid-cols-[repeat(37,minmax(0,1fr))]",
  38: "grid-cols-[repeat(38,13px)] sm:grid-cols-[repeat(38,minmax(0,1fr))]",
};

function hash(row: number, column: number, seed = 0) {
  let value = 0x165667b1 * row + 0x27d4eb2f * column + 0x9e3779b1 * seed;
  value = Math.imul(value ^ (value >>> 13), 0x4bf19f61);
  return (value ^ (value >>> 16)) >>> 0;
}

function getTier(row: number, column: number, seed: number) {
  const value = hash(row, column, seed) % 20;

  if (value < 6) return 0;
  if (value < 11) return 1;
  if (value < 15) return 2;
  if (value < 18) return 3;
  if (value < 19) return 4;
  return 5;
}

function getContributions(row: number, column: number, seed: number) {
  const [minimum, maximum] = CONTRIBUTION_RANGES[getTier(row, column, seed)];

  if (maximum === 0) return 0;
  return minimum + ((hash(row, column, seed) >>> 3) % (maximum - minimum + 1));
}

function getDateLabel(row: number, column: number, columns: number) {
  const date = new Date(
    Date.UTC(
      CURRENT_YEAR,
      0,
      1 + Math.round(((7 * column + row) / (7 * columns - 1)) * 364),
    ),
  );

  return `${MONTHS[date.getUTCMonth()]} ${date.getUTCDate()}`;
}

function getCellLabel(row: number, column: number, columns: number, seed: number) {
  const contributions = getContributions(row, column, seed);
  const date = getDateLabel(row, column, columns);

  if (contributions === 0) return `No contributions on ${date}`;
  return `${contributions} contribution${contributions === 1 ? "" : "s"} on ${date}`;
}

export interface ContributionsGridProps {
  /** Number of week columns. The BoardCN preview uses 37. */
  columns?: number;
  /** Palette used for the five non-empty contribution tiers. */
  accent?: ContributionsAccent;
  /** Pop non-empty cells in with a deterministic stagger. */
  animateIn?: boolean;
  /** Deterministically selects another dataset without changing grid geometry. */
  seed?: number;
  /** Optional accessible summary for the complete chart. */
  "aria-label"?: string;
  className?: string;
}

export function ContributionsGrid({
  columns = 37,
  accent = "violet",
  animateIn = false,
  seed = 0,
  "aria-label": ariaLabel,
  className,
}: ContributionsGridProps) {
  return (
    <div
      role={ariaLabel ? "application" : undefined}
      aria-label={ariaLabel}
      data-accent={accent}
      className={cx(
        "contributions-grid grid gap-1",
        COLUMN_CLASSES[columns] ?? COLUMN_CLASSES[37],
        className,
      )}
    >
      {Array.from({ length: 7 }, (_, row) =>
        Array.from({ length: columns }, (_, column) => {
          const label = getCellLabel(row, column, columns, seed);
          const tier = getTier(row, column, seed);
          const shouldAnimate = animateIn && tier > 0;

          return (
            <TooltipTrigger delay={0} closeDelay={0} key={`${row}-${column}`}>
              <Button
                aria-label={label}
                excludeFromTabOrder
                data-tier={tier}
                className={cx(
                  "contribution-cell aspect-square w-full cursor-default rounded-[3px] outline-none focus-visible:ring-2 focus-visible:ring-border-focus-ring",
                  shouldAnimate && "animate-cell-pop",
                )}
                style={
                  shouldAnimate
                    ? { animationDelay: `${(hash(row, column, seed) >>> 7) % 800}ms` }
                    : undefined
                }
              />
              <Tooltip>{label}</Tooltip>
            </TooltipTrigger>
          );
        }),
      )}
    </div>
  );
}

export interface ContributionsCardProps {
  stats: readonly ContributionsStat[];
  headline: number | string;
  delta?: string;
  accent?: ContributionsAccent;
  className?: string;
}

export function ContributionsCard({
  stats,
  headline,
  delta,
  accent = "violet",
  className,
}: ContributionsCardProps) {
  const [period, setPeriod] = useState("weekly");

  return (
    <section
      className={cx(
        "flex h-auto min-w-0 flex-1 flex-col gap-4 overflow-hidden rounded-2xl bg-background-secondary-default p-4 sm:h-[337px]",
        className,
      )}
    >
      <div className="flex w-full flex-col gap-0.5">
        <p className="w-full text-body-medium text-text-secondary">Contributions this year</p>
        <div className="flex w-full items-center gap-2">
          <p className="text-title-1-medium whitespace-nowrap text-text-primary">
            {typeof headline === "number" ? headline.toLocaleString() : headline}
          </p>
          {delta ? (
            <Chip variant="bold" color="lime">
              {delta}
            </Chip>
          ) : null}
        </div>
      </div>

      <div className="-mx-2 grid grid-cols-2 gap-2 sm:flex sm:items-stretch">
        {stats.map((stat) => (
          <div
            key={stat.label}
            className="flex min-w-0 flex-col items-start rounded-2lg bg-background-inner-default p-2.5 shadow-card sm:flex-1"
          >
            <p className="w-full truncate text-body-medium text-text-primary">{stat.value}</p>
            <p className="w-full truncate text-body-medium text-text-secondary">{stat.label}</p>
          </div>
        ))}
      </div>

      <div className="-mt-2 flex min-h-0 w-full flex-1 flex-col gap-1">
        <div className="flex w-full items-center justify-between">
          <p className="text-body-medium text-text-secondary">Activity</p>
          <SegmentedControl
            variant="plain"
            selectedKeys={[period]}
            onSelectionChange={(keys) => {
              const selectedPeriod = [...keys][0];
              if (selectedPeriod) setPeriod(String(selectedPeriod));
            }}
            aria-label="Activity period"
          >
            <SegmentedControlItem id="weekly">Weekly</SegmentedControlItem>
            <SegmentedControlItem id="monthly">Monthly</SegmentedControlItem>
            <SegmentedControlItem id="yearly">Yearly</SegmentedControlItem>
          </SegmentedControl>
        </div>

        <div className="flex min-h-0 w-full flex-1 flex-col gap-1.5 overflow-x-auto sm:overflow-visible">
          <div className="flex w-max flex-col gap-1.5 sm:w-full">
            <ContributionsGrid columns={37} accent={accent} />
            <div className="flex w-full justify-between text-body-2-medium text-text-tertiary">
              {MONTHS.map((month) => (
                <span key={month}>{month}</span>
              ))}
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}
"use client";

import { useState } from "react";
import { Button } from "react-aria-components";
import { Chip } from "@/components/base/badges/chip";
import {
  SegmentedControl,
  SegmentedControlItem,
} from "@/components/base/segmented-control/segmented-control";
import { Tooltip, TooltipTrigger } from "@/components/base/tooltip/tooltip";
import { cx } from "@/utils/cx";

export const ACCENTS = [
  "emerald",
  "green",
  "teal",
  "cyan",
  "blue",
  "indigo",
  "violet",
  "rose",
  "amber",
] as const;

export type ContributionsAccent = (typeof ACCENTS)[number];

export type ContributionsStat = {
  value: string;
  label: string;
};

const MONTHS = [
  "Jan",
  "Feb",
  "Mar",
  "Apr",
  "May",
  "Jun",
  "Jul",
  "Aug",
  "Sep",
  "Oct",
  "Nov",
  "Dec",
] as const;

const CONTRIBUTION_RANGES = [
  [0, 0],
  [1, 4],
  [5, 9],
  [10, 15],
  [16, 24],
  [25, 40],
] as const;

const CURRENT_YEAR = new Date().getFullYear();

const COLUMN_CLASSES: Record<number, string> = {
  37: "grid-cols-[repeat(37,13px)] sm:grid-cols-[repeat(37,minmax(0,1fr))]",
  38: "grid-cols-[repeat(38,13px)] sm:grid-cols-[repeat(38,minmax(0,1fr))]",
};

function hash(row: number, column: number, seed = 0) {
  let value = 0x165667b1 * row + 0x27d4eb2f * column + 0x9e3779b1 * seed;
  value = Math.imul(value ^ (value >>> 13), 0x4bf19f61);
  return (value ^ (value >>> 16)) >>> 0;
}

function getTier(row: number, column: number, seed: number) {
  const value = hash(row, column, seed) % 20;

  if (value < 6) return 0;
  if (value < 11) return 1;
  if (value < 15) return 2;
  if (value < 18) return 3;
  if (value < 19) return 4;
  return 5;
}

function getContributions(row: number, column: number, seed: number) {
  const [minimum, maximum] = CONTRIBUTION_RANGES[getTier(row, column, seed)];

  if (maximum === 0) return 0;
  return minimum + ((hash(row, column, seed) >>> 3) % (maximum - minimum + 1));
}

function getDateLabel(row: number, column: number, columns: number) {
  const date = new Date(
    Date.UTC(
      CURRENT_YEAR,
      0,
      1 + Math.round(((7 * column + row) / (7 * columns - 1)) * 364),
    ),
  );

  return `${MONTHS[date.getUTCMonth()]} ${date.getUTCDate()}`;
}

function getCellLabel(row: number, column: number, columns: number, seed: number) {
  const contributions = getContributions(row, column, seed);
  const date = getDateLabel(row, column, columns);

  if (contributions === 0) return `No contributions on ${date}`;
  return `${contributions} contribution${contributions === 1 ? "" : "s"} on ${date}`;
}

export interface ContributionsGridProps {
  /** Number of week columns. The BoardCN preview uses 37. */
  columns?: number;
  /** Palette used for the five non-empty contribution tiers. */
  accent?: ContributionsAccent;
  /** Pop non-empty cells in with a deterministic stagger. */
  animateIn?: boolean;
  /** Deterministically selects another dataset without changing grid geometry. */
  seed?: number;
  /** Optional accessible summary for the complete chart. */
  "aria-label"?: string;
  className?: string;
}

export function ContributionsGrid({
  columns = 37,
  accent = "violet",
  animateIn = false,
  seed = 0,
  "aria-label": ariaLabel,
  className,
}: ContributionsGridProps) {
  return (
    <div
      role={ariaLabel ? "application" : undefined}
      aria-label={ariaLabel}
      data-accent={accent}
      className={cx(
        "contributions-grid grid gap-1",
        COLUMN_CLASSES[columns] ?? COLUMN_CLASSES[37],
        className,
      )}
    >
      {Array.from({ length: 7 }, (_, row) =>
        Array.from({ length: columns }, (_, column) => {
          const label = getCellLabel(row, column, columns, seed);
          const tier = getTier(row, column, seed);
          const shouldAnimate = animateIn && tier > 0;

          return (
            <TooltipTrigger delay={0} closeDelay={0} key={`${row}-${column}`}>
              <Button
                aria-label={label}
                excludeFromTabOrder
                data-tier={tier}
                className={cx(
                  "contribution-cell aspect-square w-full cursor-default rounded-[3px] outline-none focus-visible:ring-2 focus-visible:ring-border-focus-ring",
                  shouldAnimate && "animate-cell-pop",
                )}
                style={
                  shouldAnimate
                    ? { animationDelay: `${(hash(row, column, seed) >>> 7) % 800}ms` }
                    : undefined
                }
              />
              <Tooltip>{label}</Tooltip>
            </TooltipTrigger>
          );
        }),
      )}
    </div>
  );
}

export interface ContributionsCardProps {
  stats: readonly ContributionsStat[];
  headline: number | string;
  delta?: string;
  accent?: ContributionsAccent;
  className?: string;
}

export function ContributionsCard({
  stats,
  headline,
  delta,
  accent = "violet",
  className,
}: ContributionsCardProps) {
  const [period, setPeriod] = useState("weekly");

  return (
    <section
      className={cx(
        "flex h-auto min-w-0 flex-1 flex-col gap-4 overflow-hidden rounded-2xl bg-background-secondary-default p-4 sm:h-[337px]",
        className,
      )}
    >
      <div className="flex w-full flex-col gap-0.5">
        <p className="w-full text-body-medium text-text-secondary">Contributions this year</p>
        <div className="flex w-full items-center gap-2">
          <p className="text-title-1-medium whitespace-nowrap text-text-primary">
            {typeof headline === "number" ? headline.toLocaleString() : headline}
          </p>
          {delta ? (
            <Chip variant="bold" color="lime">
              {delta}
            </Chip>
          ) : null}
        </div>
      </div>

      <div className="-mx-2 grid grid-cols-2 gap-2 sm:flex sm:items-stretch">
        {stats.map((stat) => (
          <div
            key={stat.label}
            className="flex min-w-0 flex-col items-start rounded-2lg bg-background-inner-default p-2.5 shadow-card sm:flex-1"
          >
            <p className="w-full truncate text-body-medium text-text-primary">{stat.value}</p>
            <p className="w-full truncate text-body-medium text-text-secondary">{stat.label}</p>
          </div>
        ))}
      </div>

      <div className="-mt-2 flex min-h-0 w-full flex-1 flex-col gap-1">
        <div className="flex w-full items-center justify-between">
          <p className="text-body-medium text-text-secondary">Activity</p>
          <SegmentedControl
            variant="plain"
            selectedKeys={[period]}
            onSelectionChange={(keys) => {
              const selectedPeriod = [...keys][0];
              if (selectedPeriod) setPeriod(String(selectedPeriod));
            }}
            aria-label="Activity period"
          >
            <SegmentedControlItem id="weekly">Weekly</SegmentedControlItem>
            <SegmentedControlItem id="monthly">Monthly</SegmentedControlItem>
            <SegmentedControlItem id="yearly">Yearly</SegmentedControlItem>
          </SegmentedControl>
        </div>

        <div className="flex min-h-0 w-full flex-1 flex-col gap-1.5 overflow-x-auto sm:overflow-visible">
          <div className="flex w-max flex-col gap-1.5 sm:w-full">
            <ContributionsGrid columns={37} accent={accent} />
            <div className="flex w-full justify-between text-body-2-medium text-text-tertiary">
              {MONTHS.map((month) => (
                <span key={month}>{month}</span>
              ))}
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

Props

Generated from the component's TypeScript types. Standard DOM and React Aria props are omitted.

ContributionsCard

PropTypeDefaultDescription
headlinerequiredstring | number
statsrequiredreadonly ContributionsStat[]
accent"emerald" | "green" | "teal" | "cyan" | "blue" | "indigo" | "violet" | "rose" | "amber"violet
classNamestring
deltastring

ContributionsGrid

PropTypeDefaultDescription
accent"emerald" | "green" | "teal" | "cyan" | "blue" | "indigo" | "violet" | "rose" | "amber"violetPalette used for the five non-empty contribution tiers.
animateInbooleanfalsePop non-empty cells in with a deterministic stagger.
aria-labelstringOptional accessible summary for the complete chart.
classNamestring
columnsnumber37Number of week columns. The BoardCN preview uses 37.
seednumber0Deterministically selects another dataset without changing grid geometry.