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.jsonnpx shadcn@latest add https://boardcn.dev/r/contributions-card.jsonnpm 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.
"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
| Prop | Type | Default | Description |
|---|---|---|---|
| headlinerequired | string | number | — | — |
| statsrequired | readonly ContributionsStat[] | — | — |
| accent | "emerald" | "green" | "teal" | "cyan" | "blue" | "indigo" | "violet" | "rose" | "amber" | violet | — |
| className | string | — | — |
| delta | string | — | — |
ContributionsGrid
| Prop | Type | Default | Description |
|---|---|---|---|
| accent | "emerald" | "green" | "teal" | "cyan" | "blue" | "indigo" | "violet" | "rose" | "amber" | violet | Palette used for the five non-empty contribution tiers. |
| animateIn | boolean | false | Pop non-empty cells in with a deterministic stagger. |
| aria-label | string | — | Optional accessible summary for the complete chart. |
| className | string | — | — |
| columns | number | 37 | Number of week columns. The BoardCN preview uses 37. |
| seed | number | 0 | Deterministically selects another dataset without changing grid geometry. |