Build a GitHub-Style Activity Heatmap

“Build a GitHub-style activity heatmap” is a frontend interview prompt that combines calendar math, data binning, color scaling, hover/focus interactions, and accessibility. Used in GitHub, GitLab, Strava, Duolingo, and many habit-tracking apps. Looks simple, has many subtle pieces.

Clarify scope

  • Year-long view (52 columns × 7 rows) or different time range?
  • How many color levels?
  • Hover tooltip with details?
  • Click to filter by day?
  • Mobile-friendly?

Data shape

// Input
[
  { date: "2026-01-15", count: 5 },
  { date: "2026-01-16", count: 12 },
  ...
]
// Internal
Map<"YYYY-MM-DD", count>

Calendar grid layout

The classic GitHub layout:

  • Each column is a week
  • Rows are days of week (Sun–Sat or Mon–Sun based on locale)
  • 52–53 columns for a year
  • Month labels above; day-of-week labels left

Building the grid

const today = new Date();
const start = subDays(today, 364);
// Pad to align with Sunday
const padStart = startOfWeek(start, { weekStartsOn: 0 });
const days = eachDayOfInterval({ start: padStart, end: today });

Use date-fns or dayjs. Native Date arithmetic is error-prone.

Color scaling

Bin counts into 4–5 levels. The naive approach (linear scale) is wrong because most days have low counts and a few have high. Better:

  • Quantile binning: 0, then break by 25th, 50th, 75th, 95th percentile
  • Or fixed thresholds chosen for your domain (1, 5, 10, 20+)

GitHub uses fixed thresholds tuned to typical user activity.

Rendering

SVG or CSS Grid both work:

  • SVG: better for tooltips, click handling, and animations
  • CSS Grid: simpler, accessible, no SVG knowledge needed

For 365 cells, either is fine performance-wise.

Color scheme

  • Standard: 5 shades of green from light to dark
  • Dark mode: dark green to bright green on dark background
  • Use CSS custom properties so theming is one place
  • Match your design system colors

Hover / focus tooltip

  • “5 contributions on January 15”
  • Position above or below the cell with collision detection
  • Delay 150ms before showing to avoid flicker on quick mouse-over
  • Persistent on focus for keyboard users

Keyboard navigation

  • Tab into the grid
  • Arrow keys to move between cells
  • Enter to “click” a cell
  • aria-label on each cell describes the date and count

Accessibility — the senior signal

  • Each cell has a meaningful aria-label: “5 contributions on Wednesday, January 15, 2026”
  • Roving tabindex (only one cell tab-focusable at a time)
  • Color is not the only signal — include numeric count in aria-label
  • Test with screen reader; it should not flood the user with noise

Click interaction

  • Click a day: filter the rest of the page to that day’s contributions
  • URL updates with the date
  • Clear filter to return

Empty state

Some days have zero contributions. Show as the lightest color (almost background) — not transparent, so the grid layout is preserved.

Future-day handling

  • Days in the future shown as empty / muted
  • Or hide them entirely
  • Pad calendar to the current day, not into the future

Performance

  • 365 cells render fine; do not over-engineer
  • Memoize the grid; only re-render when data changes
  • Hover tooltip in a portal to avoid stacking-context issues

Mobile considerations

  • Allow horizontal scroll if 52 columns do not fit
  • Larger touch targets (cells should be at least 12×12px)
  • Tap-to-show tooltip (no hover on touch)

Variants seen in the wild

  • Strava: minutes per day with red-orange color scale
  • Duolingo: streak indicators with day-of-month labels
  • Notion: per-database activity
  • Pinia / Habits apps: per-habit grid

Edge cases interviewers love

  • DST transitions (some days are 23 or 25 hours)
  • Leap years
  • Locale: week starts Monday in EU; Sunday in US
  • Very high outliers (one day with 100 contributions skews the legend)
  • User has no data yet (all days empty) — show friendly empty state

What separates senior from staff

Senior implementations get the grid and binning right. Staff implementations handle the keyboard and screen reader experience properly. Principal-level discussion includes the data-binning methodology (quantile vs linear), the locale-aware week start, and the responsive mobile layout.

Frequently Asked Questions

Library options?

react-calendar-heatmap is the canonical React option. For interview, build it from scratch in 30 minutes — it is a good demonstration.

How do I handle multi-year ranges?

Stack annual grids vertically with year labels. Or wrap the grid horizontally with continuous columns. GitHub used to do horizontal scrolling; now they show one year and let you switch.

Do I really need 5 color levels?

4 or 5 is the sweet spot. Fewer loses detail; more is hard to distinguish visually. GitHub uses 5 (including the empty state).

Scroll to Top