Habit Tracker Grid — Free GitHub-Style Contribution Grid Snippet

Habit Tracker Grid · Dashboards · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Real computed streak
computeStreak() scans grid data, never hardcoded.
Click-to-cycle cells
Four intensity levels via simple modulo arithmetic.
GitHub-style layout
Weeks as columns, days as rows, familiar at a glance.
Seeded demo data
Reproducible but organic-looking initial grid.
Guaranteed visible streak
Recent days seeded active so the counter has data.
Matching legend
Same four CSS levels used in cells and legend.
Day-of-week labels
Mon/Wed/Fri orientation without clutter.
Zero dependencies
Pure DOM, no charting library.

About this UI Snippet

Habit Tracker Grid — Click-to-Cycle Cells With a Real Computed Streak

Screenshot of the Habit Tracker Grid snippet rendered live

The habit tracker grid is the GitHub-contributions-style calendar that's become the default way to visualize a daily habit: one cell per day, weeks running left to right, intensity shown by color. This snippet builds the interactive version — clickable cells that cycle through four levels, plus a streak counter genuinely computed by scanning the grid data rather than hardcoded.

Four-level cells, click to cycle

Each cell holds an integer 0–3 (none, light, medium, full). Clicking a cell calls cycleCell(idx), which advances that day's value with modulo arithmetic ((grid[idx] + 1) % 4) and updates both its CSS class and its data — so the grid's visual state and its underlying array are always the same source of truth, never out of sync.

A real streak calculation, not a hardcoded number

The headline feature is computeStreak(data): it walks backward from the most recent day (the last element in the flat grid array) and counts consecutive days where the level is greater than zero, stopping at the first day with level 0. This runs fresh every time a cell is clicked via updateStreak() — there's no cached or manually-set streak number anywhere in the code. Toggle today's cell to 0 and watch the streak collapse to 0 immediately; toggle it back and the streak recalculates from the actual data.

Seeded but organic-looking data

The initial 20-week grid is generated with a small seeded pseudo-random function so it reproduces the same layout on every load while still looking like real, varied habit data. The most recent 9 days are deliberately forced to intensity 1+ so there's always a genuine trailing streak to demonstrate the counter working on load, not just after interaction.

Legend and day labels

A four-swatch "Less → More" legend mirrors the same four CSS classes used in the grid, and Mon/Wed/Fri row labels orient the grid the way GitHub's contribution graph does, without cluttering every row.

Customizing it

Swap the seeded generator for real per-day habit data from your backend, change the week count, add a month-label row along the top, or persist clicks to localStorage or an API. Pair it with activity heatmap, streak tracker, or activity rings for a fuller habit-dashboard.

Build with AI

Build, Understand, Optimize, and Extend It With AI

The interesting part of a habit tracker isn't the grid — it's making sure the streak number is trustworthy, so it's worth pasting this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and asking it to trace through computeStreak() to confirm it genuinely recalculates from the grid array on every change rather than incrementing/decrementing a separate counter that could drift out of sync with the actual cell data. It's also worth asking about edge cases: what happens at grid boundaries, whether the streak logic should account for a "grace day" some habit apps allow, or how to extend computeStreak() into a companion function that also finds the longest historical streak (not just the current trailing one) by scanning for the longest run of consecutive non-zero values anywhere in the array. From there, have it help wire the grid to localStorage or a backend so clicks persist across sessions.

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 "habit tracker grid" (GitHub-contributions-style) in plain HTML, CSS, and JavaScript — no dependencies, no CDN.

Requirements:
- Render a grid of cells representing roughly 20 weeks x 7 days (weeks as columns, days as rows), where each cell holds an integer intensity level from 0 (none) to 3 (full), styled with 4 distinct background colors.
- Clicking a cell cycles its level forward (0 -> 1 -> 2 -> 3 -> 0) using modulo arithmetic, updating both the cell's visual class and the underlying data value together.
- Implement a REAL streak-calculation function that takes the grid's flat data array and scans backward from the most recent day, counting consecutive days with intensity > 0, stopping at the first day with intensity 0. Do NOT hardcode the streak number — it must be recalculated by this function every time a cell's value changes, and must correctly drop to 0 if the most recent day is set to "none."
- Show the computed streak number prominently at the top of the card, updating live after every click.
- Include a small 4-swatch legend ("Less" to "More") using the same CSS classes as the grid cells, and light day-of-week labels (e.g. Mon/Wed/Fri) along the left edge.
- Seed the initial data with a reproducible pseudo-random generator so the grid looks organic on load, dark-theme friendly.

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 20-week grid renders with a live streak counter.
  2. 2
    Click any cellIt cycles none \u2192 light \u2192 medium \u2192 full \u2192 none.
  3. 3
    Watch the streak updatecomputeStreak() rescans the grid after every click.
  4. 4
    Zero out the most recent dayThe streak counter drops to 0 immediately.
  5. 5
    Wire up persistenceSave the grid array to localStorage or your backend.

Real-world uses

Common Use Cases

Personal habit apps
Track reading, exercise, meditation, or journaling.
Team dashboards
Pair with activity heatmap for engagement.
Gamified onboarding
Show streaks alongside streak tracker.
Wellness apps
Visualize daily check-ins over months.
Learning platforms
Show consistent study-day streaks to learners.
Developer tools
Show commit-like daily activity for any tracked event.

Got questions?

Frequently Asked Questions

It's genuinely computed. computeStreak(data) scans the grid array backward from the most recent day, counting consecutive entries greater than 0 until it hits a 0 (or the start of the array). It runs on load and again after every single cell click via updateStreak() — there is no hardcoded or cached streak value anywhere.

The streak immediately drops to 0, because computeStreak() starts scanning from the last element of the array and stops at the first zero it finds — if the most recent day is 0, the streak is 0 regardless of how many active days came before it, exactly like a real habit-tracking streak.

Each cell holds an integer 0-3. Clicking calls cycleCell(idx), which does (grid[idx] + 1) % 4 to advance it, wrapping back to 0 after 3. The cell's CSS class and its underlying array value are updated together in the same function, so they never drift out of sync.

So the demo has a real, non-zero streak visible on first load — otherwise a purely random grid might happen to end on a 0 day and the streak counter would show 0 with nothing to demonstrate. In your real implementation, this would simply be actual user data.

Save the grid array to localStorage (or your backend) inside cycleCell() after updating it, and load it back in place of the seeded generator on page load. The rendering and streak logic work unchanged against any grid array shaped as a flat list of 0-3 values.