# Build a GitHub-Style Activity Heatmap

Source: https://www.techinterview.org/post/3233475390/build-github-style-activity-heatmap/
Updated: 2026-05-06 · techinterview.org

"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).
