You Might Also Like

GitHub-Style Contribution Heatmap — Free HTML CSS JS Calendar Snippet

GitHub-Style Contribution Heatmap · Dev · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

53-week CSS Grid calendar
grid-auto-flow: column arranges 371 day cells into weekly columns with zero manual layout math.
Five-level color scale
Counts bucket into 5 discrete intensity tiers matching the familiar GitHub convention.
Month labels aligned to columns
Labels are positioned with style.gridColumn so they line up with the exact week they start.
Cursor-following tooltip
Shows exact count and locale-formatted date on hover without a tooltip library.
Sunday-aligned start date
The window rewinds to the previous Sunday so every column represents a full week.
Total contributions summary
Header text sums all counts and formats with toLocaleString() for thousands separators.
Horizontal scroll container
The grid scrolls inside its own overflow-x container on narrow screens.
Zero dependencies
Pure HTML, CSS Grid, and vanilla JavaScript — no charting or date library.

About this UI Snippet

Contribution Heatmap — CSS Grid Calendar with Level-Based Color Coding

Screenshot of the GitHub-Style Contribution Heatmap snippet rendered live

A contribution heatmap turns a year of daily activity counts into a single scannable grid, the pattern popularized by GitHub's profile page and now common on habit trackers, analytics dashboards, and streak-tracking apps. This snippet builds the full grid, color scale, and hover tooltip from scratch using CSS Grid and vanilla JavaScript — no charting library required.

Building the date grid

The script computes a 53-week window ending today, then rewinds start to the previous Sunday with start = new Date(start.getTime() - start.getDay() * DAY) so every week lands cleanly into 7 rows. It then walks forward one day at a time, pushing { date, count } objects into a flat days array — day 0 is the top-left cell, day 6 wraps to the next column, matching how grid-auto-flow: column lays cells out.

CSS Grid does the calendar layout

.chm-grid uses grid-template-rows: repeat(7, 11px) with grid-auto-flow: column, so pushing 371 cells in date order automatically arranges them into weeks-as-columns without any manual row/column math in JavaScript — the grid algorithm handles wrapping every 7 cells into a new column for you.

A five-level, not continuous, color scale

Rather than mapping raw counts to color via hsl() interpolation, levelFor() buckets counts into five discrete tiers (0, 1–2, 3–5, 6–10, 11+) matching GitHub's actual convention. Discrete levels read faster at a glance than a continuous gradient because the eye only has to distinguish five states, not infinite shades — each level maps to a fixed data-level attribute and a corresponding CSS background color.

Month labels aligned to grid columns

As the loop walks days, it detects a Sunday that starts a new month (dow === 0 && d.date.getMonth() !== lastMonthLabel) and appends a label positioned with style.gridColumn = weekIndex + 1, keeping the month row's labels aligned to the exact week column they belong to even though months don't divide evenly into 53 weeks.

A tooltip that follows the cursor

Each cell gets mouseenter/mousemove/mouseleave listeners that show a fixed-position tooltip offset from e.clientX/e.clientY, formatted with toLocaleDateString() for a locale-correct date string — cheaper than a full tooltip library for a grid with hundreds of hoverable cells.

Customizing it

Swap the random data generator for a real fetch() call to your activity API, change WEEKS to show a shorter range, or adjust the levelFor() thresholds to match your own data's distribution.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to work out the week-alignment math by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain why the start date gets rewound to the previous Sunday before the grid is built, and how grid-auto-flow: column turns a flat array of day cells into a calendar of weekly columns without manual row/column indexing. The same assistant can help optimize it too — ask whether building 371 DOM nodes up front is worth it versus a canvas-based render for very large date ranges. It's also useful for extending the heatmap: ask it to add a year selector, wire real data in from an API, or add keyboard navigation between cells for accessibility. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "GitHub-style contribution heatmap" in plain HTML, CSS, and JavaScript with no charting library.

Requirements:
- A calendar grid covering the last 53 weeks ending today, laid out with CSS Grid using 7 fixed-height rows and grid-auto-flow set to column so pushing day cells in chronological order automatically wraps into weekly columns.
- Before building the grid, rewind the start date to the closest previous Sunday so every column represents a complete 7-day week with no partial first column.
- Generate a count for each day (or accept it from an external data source) and bucket that count into five discrete intensity levels using fixed thresholds, applying a different background color per level via a data attribute rather than a computed gradient.
- Month name labels positioned above the grid, aligned to the exact week column in which that month's first Sunday falls, computed by tracking which grid column index the loop is on when the month changes.
- A hover tooltip that follows the cursor and shows the exact count and a human-readable date for the cell being hovered, positioned relative to the mouse event coordinates.
- A small legend showing "Less" to "More" with sample swatches for each of the five levels, and a header summary line showing the total count across the whole grid.
- The grid must scroll horizontally inside its own container on narrow viewports without causing the page itself to scroll sideways.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA 53-week calendar grid renders with randomly generated activity levels and a color legend.
  2. 2
    Hover a cellA tooltip shows the exact contribution count and date for that day.
  3. 3
    Scroll horizontallyOn narrow viewports the grid scrolls inside its own container so the page never scrolls sideways.
  4. 4
    Replace the data sourceSwap the random count generator in the JS for a fetch() call returning real daily counts.
  5. 5
    Adjust the color levelsEdit levelFor() thresholds and the .chm-cell[data-level] CSS colors to match your brand palette.
  6. 6
    Change the time rangeEdit the WEEKS constant to show a shorter or longer history than 53 weeks.

Real-world uses

Common Use Cases

Developer profile pages
Show a contributor's commit or PR activity, matching the pattern GitHub popularized.
Habit and streak trackers
Visualize daily habit completions over a year at a glance.
Analytics dashboards
Display daily active users, orders, or events as a scannable calendar.
Fitness and wellness apps
Show workout or meditation streaks with the same familiar heatmap language.
Content platforms
Visualize a writer's or streamer's daily posting activity over the year.
Internal team dashboards
Track daily deploys, incidents, or support tickets resolved per day.

Got questions?

Frequently Asked Questions

CSS Grid's grid-auto-flow: column wraps every 7 cells into a new column. If the first day isn't a Sunday, the first week would be a partial column and every subsequent week would be visually misaligned. Rewinding start to the prior Sunday guarantees every column is a clean 7-day week.

Replace the days.push({ date, count }) loop with data fetched from your API — for example fetch('/api/activity').then(r => r.json()), where the response is an array of { date, count } objects covering the same date range. Keep the date objects normalized to midnight so day-of-week math stays correct.

levelFor(count) buckets the raw count into one of five tiers (0, 1, 2, 3, 4) using fixed thresholds. Each cell gets a data-level attribute, and the CSS selects a background color per level with .chm-cell[data-level="N"]. Edit the thresholds in levelFor() to fit your own data's typical range.

Yes — the cell.addEventListener block already wires up mouseenter/mousemove/mouseleave for the tooltip. Add a click listener in the same forEach loop that reads d.date and d.count and opens a modal, navigates to a detail page, or expands an inline panel.

A five-level scale is easier to read at a glance across hundreds of cells because the eye only has to distinguish five known states rather than judge a continuous gradient's exact shade. It also matches the convention most developers already recognize from GitHub, so no legend explanation is needed.