More Dashboards Snippets
Streak Tracker — Habit Streak HTML CSS JS Snippet
Streak Tracker · Dashboards · Plain HTML, CSS & JS · Live preview
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.
Streak Tracker — Export as HTML, React, Vue, Angular & Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Streak Tracker</title>
<style>
*{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}
</style>
</head>
<body>
<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>
<script>
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 ✓';
}
</script>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Streak Tracker</title>
<!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
@keyframes st-pop{0%{transform:scale(1)}40%{transform:scale(1.35) rotate(-6deg)}100%{transform:scale(1)}}
@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)}}
* {
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-flame.pop {
animation:st-pop .5s cubic-bezier(.2,1.6,.4,1)
}
.st-count span {
color:#fb923c
}
.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
}
.st-day.today em {
color:#fbbf24
}
.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: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
}
</style>
</head>
<body>
<div class="[background:linear-gradient(165deg,#1e293b,#0f172a)] border border-[#334155] rounded-[20px] p-[26px] w-full max-w-[330px] text-center shadow-[0_20px_50px_rgba(0,0,0,.4)]">
<div class="mb-[22px]">
<div class="st-flame text-[56px] leading-none [filter:drop-shadow(0_6px_14px_rgba(249,115,22,.5))]" id="stFlame">🔥</div>
<div class="st-count text-2xl font-extrabold text-[#fff] mt-2"><span id="stStreak">12</span> day streak</div>
<div class="text-xs text-[#94a3b8] mt-1">Keep it going — check in every day</div>
</div>
<div class="flex justify-between gap-1.5 mb-[22px]">
<div class="st-day flex-1 flex flex-col items-center gap-1.5 done"><span class="st-dot w-[30px] h-[30px] rounded-full bg-[rgba(148,163,184,.15)] border-2 border-transparent flex items-center justify-center [transition:all_.2s]"></span><em>M</em></div>
<div class="st-day flex-1 flex flex-col items-center gap-1.5 done"><span class="st-dot w-[30px] h-[30px] rounded-full bg-[rgba(148,163,184,.15)] border-2 border-transparent flex items-center justify-center [transition:all_.2s]"></span><em>T</em></div>
<div class="st-day flex-1 flex flex-col items-center gap-1.5 done"><span class="st-dot w-[30px] h-[30px] rounded-full bg-[rgba(148,163,184,.15)] border-2 border-transparent flex items-center justify-center [transition:all_.2s]"></span><em>W</em></div>
<div class="st-day flex-1 flex flex-col items-center gap-1.5 today"><span class="st-dot w-[30px] h-[30px] rounded-full bg-[rgba(148,163,184,.15)] border-2 border-transparent flex items-center justify-center [transition:all_.2s]"></span><em>T</em></div>
<div class="st-day flex-1 flex flex-col items-center gap-1.5"><span class="st-dot w-[30px] h-[30px] rounded-full bg-[rgba(148,163,184,.15)] border-2 border-transparent flex items-center justify-center [transition:all_.2s]"></span><em>F</em></div>
<div class="st-day flex-1 flex flex-col items-center gap-1.5"><span class="st-dot w-[30px] h-[30px] rounded-full bg-[rgba(148,163,184,.15)] border-2 border-transparent flex items-center justify-center [transition:all_.2s]"></span><em>S</em></div>
<div class="st-day flex-1 flex flex-col items-center gap-1.5"><span class="st-dot w-[30px] h-[30px] rounded-full bg-[rgba(148,163,184,.15)] border-2 border-transparent flex items-center justify-center [transition:all_.2s]"></span><em>S</em></div>
</div>
<div class="flex gap-3 mb-5">
<div class="st-stat flex-1 bg-[rgba(148,163,184,.08)] border border-[rgba(148,163,184,.12)] rounded-xl p-3"><span id="stBest">21</span><small>Best streak</small></div>
<div class="st-stat flex-1 bg-[rgba(148,163,184,.08)] border border-[rgba(148,163,184,.12)] rounded-xl p-3"><span id="stTotal">148</span><small>Total days</small></div>
</div>
<button class="st-btn w-full p-[13px] [background:linear-gradient(135deg,#f97316,#ea580c)] text-[#fff] border-0 rounded-[13px] text-[15px] font-extrabold cursor-pointer font-[inherit] [transition:filter_.15s,transform_.1s]" id="stBtn" onclick="checkIn()">Check in today</button>
</div>
<script>
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 ✓';
}
</script>
</body>
</html>import React, { useEffect } from 'react';
// CSS — optionally move to StreakTracker.module.css
const css = `
*{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}
`;
export default function StreakTracker() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
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 ✓';
}
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
// Expose handlers used by inline JSX events to the global scope
if (typeof checkIn === 'function') window.checkIn = checkIn;
}, []);
return (
<>
<style>{css}</style>
<div className="st-card">
<div className="st-hero">
<div className="st-flame" id="stFlame">🔥</div>
<div className="st-count"><span id="stStreak">12</span> day streak</div>
<div className="st-sub">Keep it going — check in every day</div>
</div>
<div className="st-week">
<div className="st-day done"><span className="st-dot"></span><em>M</em></div>
<div className="st-day done"><span className="st-dot"></span><em>T</em></div>
<div className="st-day done"><span className="st-dot"></span><em>W</em></div>
<div className="st-day today"><span className="st-dot"></span><em>T</em></div>
<div className="st-day"><span className="st-dot"></span><em>F</em></div>
<div className="st-day"><span className="st-dot"></span><em>S</em></div>
<div className="st-day"><span className="st-dot"></span><em>S</em></div>
</div>
<div className="st-stats">
<div className="st-stat"><span id="stBest">21</span><small>Best streak</small></div>
<div className="st-stat"><span id="stTotal">148</span><small>Total days</small></div>
</div>
<button className="st-btn" id="stBtn" onClick={(event) => { checkIn() }}>Check in today</button>
</div>
</>
);
}import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+
export default function StreakTracker() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
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 ✓';
}
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
// Expose handlers used by inline JSX events to the global scope
if (typeof checkIn === 'function') window.checkIn = checkIn;
}, []);
return (
<>
<style>{`
@keyframes st-pop{0%{transform:scale(1)}40%{transform:scale(1.35) rotate(-6deg)}100%{transform:scale(1)}}
@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)}}
* {
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-flame.pop {
animation:st-pop .5s cubic-bezier(.2,1.6,.4,1)
}
.st-count span {
color:#fb923c
}
.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
}
.st-day.today em {
color:#fbbf24
}
.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: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
}
`}</style>
<div className="[background:linear-gradient(165deg,#1e293b,#0f172a)] border border-[#334155] rounded-[20px] p-[26px] w-full max-w-[330px] text-center shadow-[0_20px_50px_rgba(0,0,0,.4)]">
<div className="mb-[22px]">
<div className="st-flame text-[56px] leading-none [filter:drop-shadow(0_6px_14px_rgba(249,115,22,.5))]" id="stFlame">🔥</div>
<div className="st-count text-2xl font-extrabold text-[#fff] mt-2"><span id="stStreak">12</span> day streak</div>
<div className="text-xs text-[#94a3b8] mt-1">Keep it going — check in every day</div>
</div>
<div className="flex justify-between gap-1.5 mb-[22px]">
<div className="st-day flex-1 flex flex-col items-center gap-1.5 done"><span className="st-dot w-[30px] h-[30px] rounded-full bg-[rgba(148,163,184,.15)] border-2 border-transparent flex items-center justify-center [transition:all_.2s]"></span><em>M</em></div>
<div className="st-day flex-1 flex flex-col items-center gap-1.5 done"><span className="st-dot w-[30px] h-[30px] rounded-full bg-[rgba(148,163,184,.15)] border-2 border-transparent flex items-center justify-center [transition:all_.2s]"></span><em>T</em></div>
<div className="st-day flex-1 flex flex-col items-center gap-1.5 done"><span className="st-dot w-[30px] h-[30px] rounded-full bg-[rgba(148,163,184,.15)] border-2 border-transparent flex items-center justify-center [transition:all_.2s]"></span><em>W</em></div>
<div className="st-day flex-1 flex flex-col items-center gap-1.5 today"><span className="st-dot w-[30px] h-[30px] rounded-full bg-[rgba(148,163,184,.15)] border-2 border-transparent flex items-center justify-center [transition:all_.2s]"></span><em>T</em></div>
<div className="st-day flex-1 flex flex-col items-center gap-1.5"><span className="st-dot w-[30px] h-[30px] rounded-full bg-[rgba(148,163,184,.15)] border-2 border-transparent flex items-center justify-center [transition:all_.2s]"></span><em>F</em></div>
<div className="st-day flex-1 flex flex-col items-center gap-1.5"><span className="st-dot w-[30px] h-[30px] rounded-full bg-[rgba(148,163,184,.15)] border-2 border-transparent flex items-center justify-center [transition:all_.2s]"></span><em>S</em></div>
<div className="st-day flex-1 flex flex-col items-center gap-1.5"><span className="st-dot w-[30px] h-[30px] rounded-full bg-[rgba(148,163,184,.15)] border-2 border-transparent flex items-center justify-center [transition:all_.2s]"></span><em>S</em></div>
</div>
<div className="flex gap-3 mb-5">
<div className="st-stat flex-1 bg-[rgba(148,163,184,.08)] border border-[rgba(148,163,184,.12)] rounded-xl p-3"><span id="stBest">21</span><small>Best streak</small></div>
<div className="st-stat flex-1 bg-[rgba(148,163,184,.08)] border border-[rgba(148,163,184,.12)] rounded-xl p-3"><span id="stTotal">148</span><small>Total days</small></div>
</div>
<button className="st-btn w-full p-[13px] [background:linear-gradient(135deg,#f97316,#ea580c)] text-[#fff] border-0 rounded-[13px] text-[15px] font-extrabold cursor-pointer font-[inherit] [transition:filter_.15s,transform_.1s]" id="stBtn" onClick={(event) => { checkIn() }}>Check in today</button>
</div>
</>
);
}<template>
<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" @click="checkIn()">Check in today</button>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
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 ✓';
}
</script>
<style scoped>
*{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}
</style>// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-streak-tracker',
standalone: true,
imports: [CommonModule],
encapsulation: ViewEncapsulation.None,
template: `
<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" (click)="checkIn()">Check in today</button>
</div>
`,
styles: [`
*{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}
`]
})
export class StreakTrackerComponent implements AfterViewInit {
ngAfterViewInit(): void {
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 ✓';
}
// Bind inline-handler functions to the component so the template can call them
Object.assign(this, { checkIn });
}
}