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