Habit tracker streaks
What this is: A tiny habit tracker you can drop into your site as a single file. It tracks daily habits, calculates streaks, and shows a simple “heatmap” calendar — all without accounts or a database.
Why it’s useful: Most habit apps are overkill when you just want to build consistency. This script is fast, private (local-only), and works great as a personal /tools page.
What it does:
- Create habits (drink water, walk, code, journal, etc.)
- One-click “Done today” toggle
- Streak tracking (current + best)
- 30-day heatmap calendar per habit
- Exports/imports your data as JSON (optional but handy)
Install:
- Create:
/tools/habits/ - Save the file below as:
/tools/habits/index.html - Visit:
/tools/habits/
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Tiny Habit Tracker (Streaks)</title>
<meta name="description" content="A lightweight habit tracker with streaks and a simple calendar heatmap. No login, no database—data stays in your browser." />
<meta name="robots" content="index,follow" />
<style>
:root{
--bg:#0b0f16; --panel:#111a26; --panel2:#0c1420;
--text:#e8eef8; --muted:rgba(232,238,248,.72);
--line:rgba(255,255,255,.10); --accent:#2a8cff;
--shadow:0 18px 55px rgba(0,0,0,.45); --r:16px;
--ok:#7CFF9A; --bad:#FF7C7C;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--text);font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif}
.wrap{max-width:1050px;margin:0 auto;padding:18px}
.card{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);padding:14px;margin:12px 0;box-shadow:var(--shadow)}
.top{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;align-items:center}
.pill{display:inline-block;padding:4px 10px;border-radius:999px;background:rgba(42,140,255,.14);border:1px solid rgba(42,140,255,.25);font-weight:900;font-size:12px}
.muted{color:var(--muted)}
.grid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:12px}
.c7{grid-column:span 7}
.c5{grid-column:span 5}
@media(max-width:980px){.c7,.c5{grid-column:span 12}}
input{
width:100%;padding:10px 12px;border-radius:12px;border:1px solid var(--line);
background:var(--panel2);color:var(--text);outline:none;
font:700 13px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
}
.btn{
display:inline-flex;align-items:center;justify-content:center;gap:8px;
border:0;border-radius:12px;padding:10px 12px;background:var(--accent);
color:#06101a;font-weight:900;cursor:pointer;
}
.btn2{
display:inline-flex;align-items:center;justify-content:center;gap:8px;
border:1px solid var(--line);border-radius:12px;padding:10px 12px;background:transparent;
color:var(--text);font-weight:900;cursor:pointer;
}
.habit{
display:grid;grid-template-columns:1fr auto;gap:12px;align-items:center;
padding:12px;border:1px solid rgba(255,255,255,.10);border-radius:16px;background:rgba(255,255,255,.03);
margin:10px 0;
}
.hname{font-weight:950;font-size:16px}
.meta{font-size:12px;color:var(--muted);margin-top:4px}
.toggle{
display:inline-flex;align-items:center;gap:10px;
border:1px solid rgba(255,255,255,.12);
background:transparent;color:var(--text);
border-radius:999px;padding:10px 12px;font-weight:950;cursor:pointer;
white-space:nowrap;
}
.dot{
width:14px;height:14px;border-radius:999px;border:2px solid rgba(255,255,255,.25);
background:transparent;
}
.toggle.on{border-color:rgba(124,255,154,.35)}
.toggle.on .dot{border-color:rgba(124,255,154,.65);background:rgba(124,255,154,.22)}
.heat{
margin-top:10px;
display:grid;grid-template-columns:repeat(15, 1fr);gap:6px;
}
.cell{
aspect-ratio:1/1;border-radius:7px;border:1px solid rgba(255,255,255,.08);
background:rgba(255,255,255,.03);
}
.cell.on{background:rgba(124,255,154,.18);border-color:rgba(124,255,154,.28)}
.cell.off{background:rgba(255,255,255,.03)}
.rowline{height:1px;background:var(--line);margin:12px 0}
textarea{
width:100%;min-height:140px;resize:vertical;
padding:10px 12px;border-radius:14px;border:1px solid var(--line);
background:var(--panel2);color:var(--text);outline:none;
font:650 12px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
}
.mini{font-size:12px;color:var(--muted)}
.danger{color:#ffb3b3}
.rightActions{display:flex;gap:10px;flex-wrap:wrap}
</style>
</head>
<body>
<div class="wrap">
<div class="card">
<div class="top">
<div>
<div class="pill">No Login</div>
<h1 style="margin:10px 0 6px;font-size:22px">Tiny Habit Tracker</h1>
<div class="muted">One-click habits, streaks, and a 30-day heatmap. Data stays in your browser.</div>
</div>
<div class="rightActions">
<button class="btn2" id="exportBtn" type="button">⬇️ Export</button>
<button class="btn2" id="importBtn" type="button">⬆️ Import</button>
<button class="btn2" id="resetBtn" type="button">🧹 Reset</button>
</div>
</div>
</div>
<div class="grid">
<div class="card c7">
<b>Add a habit</b>
<div class="muted" style="margin:6px 0 10px">Examples: Walk 20 min, No soda, Journal, Push-ups, Code daily.</div>
<div style="display:grid;gap:10px">
<input id="habitName" placeholder="Habit name..." maxlength="60" />
<button class="btn" id="addBtn" type="button">➕ Add habit</button>
</div>
<div class="rowline"></div>
<div id="list"></div>
</div>
<div class="card c5">
<b>How it works</b>
<div class="muted" style="margin-top:8px;line-height:1.55;font-size:13px">
<ul style="margin:0;padding-left:18px">
<li><b>Done today</b> stores today’s date for that habit.</li>
<li><b>Streak</b> counts consecutive days ending today.</li>
<li><b>Best</b> is the highest streak ever.</li>
<li><b>Heatmap</b> shows the last 30 days (green = done).</li>
</ul>
<div style="margin-top:10px">
This is intentionally simple. If you want goals like “3x/week” or reminders, that’s a bigger version.
</div>
</div>
<div class="rowline"></div>
<b>Backup / transfer</b>
<div class="mini" style="margin:8px 0 8px">Export your data (JSON). Import it on another device.</div>
<textarea id="io" placeholder="Export output appears here..."></textarea>
<div class="mini danger" style="margin-top:8px">Reset clears all habits from this browser.</div>
</div>
</div>
</div>
<script>
(function(){
var KEY = "tinyHabits:v1";
function todayISO(){
var d = new Date();
d.setHours(0,0,0,0);
return d.toISOString().slice(0,10);
}
function addDaysISO(iso, days){
var d = new Date(iso + "T00:00:00Z");
d.setUTCDate(d.getUTCDate() + days);
return d.toISOString().slice(0,10);
}
function load(){
try {
var j = JSON.parse(localStorage.getItem(KEY) || "");
if (!j || !Array.isArray(j.habits)) return {habits:[]};
return j;
} catch(e){
return {habits:[]};
}
}
function save(state){
localStorage.setItem(KEY, JSON.stringify(state));
}
function uid(){
return Math.random().toString(16).slice(2) + Date.now().toString(16);
}
function dedupeDates(arr){
var seen = Object.create(null);
var out = [];
for (var i=0;i<arr.length;i++){
var x = arr[i];
if (!/^\d{4}-\d{2}-\d{2}$/.test(x)) continue;
if (seen[x]) continue;
seen[x]=1;
out.push(x);
}
out.sort();
return out;
}
function currentStreak(dates){
// streak ending today
var set = Object.create(null);
dates.forEach(function(x){ set[x]=1; });
var t = todayISO();
var s = 0;
while (set[t]) {
s++;
t = addDaysISO(t, -1);
}
return s;
}
function bestStreak(dates){
if (!dates.length) return 0;
// dates sorted
var best = 1, cur = 1;
for (var i=1;i<dates.length;i++){
var prev = dates[i-1];
var curD = dates[i];
if (addDaysISO(prev, 1) === curD) {
cur++;
} else {
best = Math.max(best, cur);
cur = 1;
}
}
best = Math.max(best, cur);
return best;
}
function lastNDays(n){
var out = [];
var t = todayISO();
for (var i=n-1;i>=0;i--){
out.push(addDaysISO(t, -i));
}
return out;
}
function render(){
var state = load();
var list = document.getElementById("list");
list.innerHTML = "";
if (!state.habits.length){
var empty = document.createElement("div");
empty.className = "muted";
empty.textContent = "No habits yet. Add one above.";
list.appendChild(empty);
return;
}
var days = lastNDays(30);
state.habits.forEach(function(h){
var dates = dedupeDates(h.dates || []);
var st = currentStreak(dates);
var best = bestStreak(dates);
var doneToday = dates.indexOf(todayISO()) !== -1;
var wrap = document.createElement("div");
wrap.className = "habit";
var left = document.createElement("div");
var name = document.createElement("div");
name.className = "hname";
name.textContent = h.name || "Habit";
var meta = document.createElement("div");
meta.className = "meta";
meta.innerHTML = 'Current streak: <b>' + st + '</b> • Best: <b>' + best + '</b>';
var heat = document.createElement("div");
heat.className = "heat";
var set = Object.create(null);
dates.forEach(function(x){ set[x]=1; });
days.forEach(function(d){
var cell = document.createElement("div");
cell.className = "cell " + (set[d] ? "on" : "off");
cell.title = d + (set[d] ? " ✓" : "");
heat.appendChild(cell);
});
left.appendChild(name);
left.appendChild(meta);
left.appendChild(heat);
var btn = document.createElement("button");
btn.className = "toggle" + (doneToday ? " on" : "");
btn.type = "button";
btn.innerHTML = '<span class="dot"></span>' + (doneToday ? "Done today" : "Mark done");
btn.addEventListener("click", function(){
var s2 = load();
var hh = s2.habits.find(function(x){ return x.id === h.id; });
if (!hh) return;
var t = todayISO();
hh.dates = dedupeDates(hh.dates || []);
var idx = hh.dates.indexOf(t);
if (idx === -1) hh.dates.push(t);
else hh.dates.splice(idx, 1);
hh.dates = dedupeDates(hh.dates);
save(s2);
render();
});
// right side (small delete)
var right = document.createElement("div");
right.style.display = "grid";
right.style.gap = "10px";
right.style.justifyItems = "end";
var del = document.createElement("button");
del.className = "btn2";
del.type = "button";
del.textContent = "🗑 Delete";
del.addEventListener("click", function(){
if (!confirm("Delete this habit?")) return;
var s3 = load();
s3.habits = (s3.habits || []).filter(function(x){ return x.id !== h.id; });
save(s3);
render();
});
right.appendChild(btn);
right.appendChild(del);
wrap.appendChild(left);
wrap.appendChild(right);
list.appendChild(wrap);
});
}
document.getElementById("addBtn").addEventListener("click", function(){
var name = document.getElementById("habitName").value.trim();
if (name.length < 2) return alert("Enter a habit name.");
var state = load();
state.habits = state.habits || [];
state.habits.push({id:uid(), name:name.slice(0,60), dates:[]});
save(state);
document.getElementById("habitName").value = "";
render();
});
document.getElementById("exportBtn").addEventListener("click", function(){
var state = load();
var out = JSON.stringify(state, null, 2);
document.getElementById("io").value = out;
});
document.getElementById("importBtn").addEventListener("click", function(){
var raw = document.getElementById("io").value.trim();
if (!raw) return alert("Paste exported JSON into the box first.");
try {
var j = JSON.parse(raw);
if (!j || !Array.isArray(j.habits)) return alert("Invalid format.");
// normalize
j.habits = j.habits.map(function(h){
return {
id: String(h.id || uid()),
name: String(h.name || "Habit").slice(0,60),
dates: (Array.isArray(h.dates) ? h.dates : [])
};
});
save(j);
render();
alert("Imported.");
} catch(e){
alert("Invalid JSON.");
}
});
document.getElementById("resetBtn").addEventListener("click", function(){
if (!confirm("Reset all habits for this browser?")) return;
localStorage.removeItem(KEY);
document.getElementById("io").value = "";
render();
});
render();
})();
</script>
</body>
</html>
Comments (0)
No comments yet — be the first.