Habit tracker streaks

February 14, 2026 NEW

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:

  1. Create: /tools/habits/
  2. Save the file below as: /tools/habits/index.html
  3. 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.

← Back to all scripts