webdevpuneet.com

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

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

Category🛠 Dev

Copy & paste

Source Code

<div class="chm-card">
  <div class="chm-head">
    <div>
      <h3>Contribution activity</h3>
      <p id="chmTotal">— contributions in the last year</p>
    </div>
    <div class="chm-legend">
      <span>Less</span>
      <span class="chm-swatch" data-level="0"></span>
      <span class="chm-swatch" data-level="1"></span>
      <span class="chm-swatch" data-level="2"></span>
      <span class="chm-swatch" data-level="3"></span>
      <span class="chm-swatch" data-level="4"></span>
      <span>More</span>
    </div>
  </div>
  <div class="chm-scroll">
    <div class="chm-months" id="chmMonths"></div>
    <div class="chm-body">
      <div class="chm-days">
        <span>Mon</span><span></span><span>Wed</span><span></span><span>Fri</span><span></span><span></span>
      </div>
      <div class="chm-grid" id="chmGrid"></div>
    </div>
  </div>
  <div class="chm-tooltip" id="chmTooltip" hidden></div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

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.

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 pull request history in a 53-week grid, with grid-auto-flow: column arranging 371 cells into weekly columns.
Habit and streak trackers
Visualise daily habit completion with five discrete intensity levels in a habit or streak tracker, matching the familiar colour scale.
Analytics dashboards
Display daily active users or orders as a year at a glance, with month labels placed using style.gridColumn.
Fitness and wellness apps
Show workout or meditation streaks, with a cursor-following tooltip giving the exact count and a locale-formatted date.
Content and team dashboards
Visualise a writer's daily publishing or a team's deploys and incidents, built with no charting library.
Related: Text Diff Checker
See the Text Diff Checker for a related dev pattern worth pairing with this one.
Related: Base64 & URL-Safe Encoder/Decoder
See the Base64 & URL-Safe Encoder/Decoder for a related dev pattern worth pairing with this one.
Related: UUID / ULID Generator & Validator
See the UUID / ULID Generator & Validator for a related dev pattern worth pairing with this one.
Related: cURL Command Builder
See the cURL Command Builder for a related dev pattern worth pairing with this one.
Related: Number Base Converter
See the Number Base Converter for a related dev pattern worth pairing with this one.
Related: Semver Range Checker
See the Semver Range Checker for a related dev pattern worth pairing with this one.

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.