Streak Tracker — Habit Streak HTML CSS JS Snippet
Streak Tracker · Dashboards · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="st-card">
<div class="st-hero">
<div class="st-flame" id="stFlame">🔥</div>
<div class="st-count"><span id="stStreak">12</span> day streak</div>
<div class="st-sub">Keep it going — check in every day</div>
</div>
<div class="st-week">
<div class="st-day done"><span class="st-dot"></span><em>M</em></div>
<div class="st-day done"><span class="st-dot"></span><em>T</em></div>
<div class="st-day done"><span class="st-dot"></span><em>W</em></div>
<div class="st-day today"><span class="st-dot"></span><em>T</em></div>
<div class="st-day"><span class="st-dot"></span><em>F</em></div>
<div class="st-day"><span class="st-dot"></span><em>S</em></div>
<div class="st-day"><span class="st-dot"></span><em>S</em></div>
</div>
<div class="st-stats">
<div class="st-stat"><span id="stBest">21</span><small>Best streak</small></div>
<div class="st-stat"><span id="stTotal">148</span><small>Total days</small></div>
</div>
<button class="st-btn" id="stBtn" onclick="checkIn()">Check in today</button>
</div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0f172a;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.st-card{background:linear-gradient(165deg,#1e293b,#0f172a);border:1px solid #334155;border-radius:20px;padding:26px;width:100%;max-width:330px;text-align:center;box-shadow:0 20px 50px rgba(0,0,0,.4)}
.st-hero{margin-bottom:22px}
.st-flame{font-size:56px;line-height:1;filter:drop-shadow(0 6px 14px rgba(249,115,22,.5))}
.st-flame.pop{animation:st-pop .5s cubic-bezier(.2,1.6,.4,1)}
@keyframes st-pop{0%{transform:scale(1)}40%{transform:scale(1.35) rotate(-6deg)}100%{transform:scale(1)}}
.st-count{font-size:24px;font-weight:800;color:#fff;margin-top:8px}
.st-count span{color:#fb923c}
.st-sub{font-size:12px;color:#94a3b8;margin-top:4px}
.st-week{display:flex;justify-content:space-between;gap:6px;margin-bottom:22px}
.st-day{flex:1;display:flex;flex-direction:column;align-items:center;gap:6px}
.st-dot{width:30px;height:30px;border-radius:50%;background:rgba(148,163,184,.15);border:2px solid transparent;display:flex;align-items:center;justify-content:center;transition:all .2s}
.st-day em{font-size:11px;font-weight:700;color:#64748b;font-style:normal}
.st-day.done .st-dot{background:linear-gradient(160deg,#fb923c,#ea580c);box-shadow:0 4px 10px rgba(234,88,12,.4)}
.st-day.done .st-dot::after{content:'🔥';font-size:14px}
.st-day.done em{color:#fb923c}
.st-day.today .st-dot{border-color:#fb923c;background:rgba(251,146,60,.12);animation:st-pulse 1.8s ease-out infinite}
@keyframes st-pulse{0%{box-shadow:0 0 0 0 rgba(251,146,60,.5)}70%{box-shadow:0 0 0 9px rgba(251,146,60,0)}100%{box-shadow:0 0 0 0 rgba(251,146,60,0)}}
.st-day.today em{color:#fbbf24}
.st-stats{display:flex;gap:12px;margin-bottom:20px}
.st-stat{flex:1;background:rgba(148,163,184,.08);border:1px solid rgba(148,163,184,.12);border-radius:12px;padding:12px}
.st-stat span{display:block;font-size:20px;font-weight:800;color:#f1f5f9;font-variant-numeric:tabular-nums}
.st-stat small{font-size:11px;color:#94a3b8;font-weight:600}
.st-btn{width:100%;padding:13px;background:linear-gradient(135deg,#f97316,#ea580c);color:#fff;border:none;border-radius:13px;font-size:15px;font-weight:800;cursor:pointer;font-family:inherit;transition:filter .15s,transform .1s}
.st-btn:hover:not(:disabled){filter:brightness(1.08)}
.st-btn:active:not(:disabled){transform:scale(.98)}
.st-btn:disabled{background:rgba(148,163,184,.2);color:#94a3b8;cursor:default}var streak = 12, best = 21, total = 148, checked = false;
function checkIn() {
if (checked) return;
checked = true;
var today = document.querySelector('.st-day.today');
if (today) { today.classList.remove('today'); today.classList.add('done'); }
streak++; total++;
document.getElementById('stStreak').textContent = streak;
document.getElementById('stTotal').textContent = total;
if (streak > best) { best = streak; document.getElementById('stBest').textContent = best; }
var flame = document.getElementById('stFlame');
flame.classList.remove('pop');
void flame.offsetWidth;
flame.classList.add('pop');
var btn = document.getElementById('stBtn');
btn.disabled = true;
btn.textContent = 'Checked in today ✓';
}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.
What's included
Features
checked flag makes checkIn fire once, converting today to done and locking the button, like real streak apps.cubic-bezier scale+rotate keyframe on the flame via a forced-reflow restart — the reward moment.box-shadow keyframe — zero JS cost and perfectly steady.streak, best, total, and checked hold all state, so seeding from a backend is a few assignments.About this UI Snippet
Streak Tracker — Flame Counter, 7-Day Check-In Row & One-Tap Daily Check-In
Streaks are one of the most powerful engagement mechanics in apps — Duolingo, Snapchat, and fitness trackers all use them because the fear of breaking a run keeps people coming back daily. A streak tracker shows the current run, a visual week of check-ins, and a single action to extend it. This snippet implements that in plain HTML, CSS, and vanilla JavaScript: an animated flame counter, a seven-day dot row with completed/today/upcoming states, best and total stats, and a check-in button that extends the streak with a satisfying flame pop.
The week row: three day states
The seven dots represent the current week. Completed days (.done) show a filled orange gradient with a tiny flame and a coloured label; today (.today) is an outlined dot with a continuous pulsing ring drawing attention to the action; upcoming days stay muted. This instantly communicates "you've done Mon–Wed, today is Thursday, here's the rest of the week" — the at-a-glance progress that makes streaks motivating.
One-tap check-in
checkIn is guarded so it only fires once: it converts today's dot from the pulsing today state to a completed done flame, increments the streak and total counters, updates the best streak if the new run exceeds it, and disables the button to "Checked in today ✓". A checked flag prevents double check-ins, mirroring how real streak apps lock the action until the next day.
Flame celebration
The moment you check in, the big flame plays a spring st-pop keyframe — scaling up with a slight rotation and settling back via a cubic-bezier overshoot. It is re-triggered with the remove-class / force-reflow / add-class pattern so it fires on every check-in. That little reward is the dopamine hit that reinforces the habit. The today dot's pulse is a pure-CSS box-shadow animation, so it costs nothing and never drifts.
Stats that frame the streak
Best streak and total days sit below the week, giving context — a personal record to beat and a sense of long-term commitment. The current streak is the hero number beside the flame, in the accent colour.
Everything is driven by a few variables (streak, best, total, checked), so wiring it to real data is just seeding those from your backend and persisting the check-in. In production you would also reset the streak to zero if a day was missed (compare last-check-in date to today). Pair this with a profile completion meter for onboarding, an activity heatmap for long-term history, or a stats card for other metrics.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA streak card appears with a flame, "12 day streak", a week row (Mon–Wed completed, Thursday pulsing), best/total stats, and a check-in button.
- 2Read the weekCompleted days show filled flame dots, today pulses with an outlined ring, and upcoming days stay muted.
- 3Check inClick "Check in today" — today's dot fills with a flame, the streak ticks to 13, total updates, and the big flame pops.
- 4See the button lockThe button disables to "Checked in today ✓" so you cannot check in twice in one day.
- 5Watch best updateIf your new streak beats the record, the "Best streak" stat updates to match.
- 6Wire real dataSeed
streak,best, andtotalfrom your backend and persist the check-in date; reset the streak if a day is missed.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Store the streak, best, total, and the last check-in date (server-side or in localStorage). On load, compare today to the last check-in: if it is the next day, allow check-in; if more than one day has passed, reset the streak to 0 before rendering. Save the new values after each check-in. This date logic is what makes a streak real rather than a counter.
Render more dots (a 30-day grid) or switch to an activity heatmap for a calendar view. Keep the seven-day row as the focused "this week" widget and link to the fuller history — most apps show both: a compact current week and an expandable full calendar.
Always validate check-ins server-side against the user's stored timezone (or UTC day boundaries), not the client clock, since device time can be changed. The client UI is optimistic, but the authoritative streak count and the "already checked in today" rule must be enforced on the server.
Make the check-in a real <button> (it is) so it is keyboard-operable, and announce the new streak via an aria-live="polite" region after check-in. Give each day dot an aria-label ("Monday: completed", "Thursday: today") since the state is conveyed by colour and a flame glyph; ensure the disabled state is communicated, not just styled.
In React, hold streak, best, total, and checkedToday in useState, render the week from a days array, and trigger the flame pop by toggling a class/key on check-in. In Vue, use refs and :class bindings with a method. In Angular, track state on the component and bind [class.done]/[disabled]. The pop/pulse keyframes port unchanged.