Offline-first habit tracker with a GitHub-style yearly heatmap. Built as a real PWA — installable, survives airplane mode, no backend.
Live demo: demo-habit.jcortes.dev
| Light | Dark |
|---|---|
Most habit-tracker tutorials are thin wrappers over a TODO list and a server. This one inverts the constraints: no server, no auth, no sync. The data layer is IndexedDB. The heatmap is hand-built with D3 utilities. Everything works on a plane.
Three things this project tries to do well:
- Offline-first as a real constraint, not a label. Every interaction goes through Dexie. The app loads, mutates, and renders without a network round-trip. A service worker precaches the shell so first-paint also works offline.
- A custom GitHub-style heatmap, built from scratch with
d3-scale+d3-time-format. Nonivo, norecharts. The point is showing I can compose SVG, scales, and date formats directly — not gluing chart props together. - PWA install + offline scored on Lighthouse. Installable on Chrome / Edge desktop and Android. Verified by running the production build in airplane mode end-to-end.
Counter-intuitive on the surface — Next.js is famous for SSR and server components, and this project has neither. The reasons it still earns its weight:
- App Router gives clean static export for the few pages this project needs (heatmap, settings, about) without bringing in a separate router.
- Vercel deploy is first-class and gives the production-grade headers a PWA needs (cache, manifest, service worker scope) without manual config.
- Image and font handling are solved out of the box — important for the install icon set and the heatmap's tooltip font.
- Future-proof. If this app ever grew a sync layer, Server Actions and Route Handlers are right there.
See docs/DECISIONS.md — ADR-001 (Dexie over a backend) and ADR-002 (Next.js over Vite here) cover the full reasoning.
- 📅 GitHub-style yearly heatmap, 365 cells, intensity-scaled, hand-rendered SVG via D3 utilities
- 🔁 Per-habit drilldown — click a habit to see its own heatmap; click again for the aggregated view
- ✅ One-tap toggle for today, with the current streak rendered inline
- 💾 IndexedDB persistence via Dexie — your data never leaves the device
✈️ Real offline support — installs as a PWA, works in airplane mode, survives a reload- ⌨️ Full keyboard navigation across the heatmap with
aria-labelper cell - 🌙 Dark mode with system preference fallback and anti-FOUC inline script
- 📱 Responsive from 320px upward
- ♿ WCAG 2.1 AA —
axe-coreruns against the rendered page in CI
| Layer | Choice | Why |
|---|---|---|
| Framework | Next.js 16 (App Router) | Static export + Vercel PWA headers + room to grow |
| UI library | React 19 | Stable in Next 16 |
| Language | TypeScript (strict mode) | Type safety end-to-end, including Dexie schema |
| Styling | Tailwind CSS v4 | Native CSS variables, no PostCSS config |
| Data | Dexie (IndexedDB wrapper) | Lightweight, typed, survives reload, offline-first |
| Visualization | D3 utilities (d3-scale, d3-time-format) |
Compose scales + SVG directly, no chart library |
| PWA | Serwist (@serwist/next) |
Modern successor to next-pwa, first-class Next 16 support |
| State | useState + Dexie live queries |
No global store |
| Testing | Vitest + Playwright + @axe-core/playwright + fake-indexeddb |
Unit + integration + E2E + a11y |
| Linting | Biome | Replaces ESLint + Prettier in one tool |
| Hosting | Vercel | Static deploy, edge CDN, PWA headers |
See docs/ARCHITECTURE.md for the deeper rationale.
Requirements: Node.js 20+ and pnpm 9+.
pnpm install
pnpm dev # http://localhost:3000Other scripts:
pnpm build # production build (Next.js, webpack — Serwist needs it)
pnpm start # serve the production build
pnpm test # unit tests (Vitest)
pnpm test:e2e # end-to-end tests (Playwright + axe)
pnpm check # Biome (lint + format)
pnpm typecheck # tsc --noEmitsrc/
├── app/
│ ├── layout.tsx # root layout, metadata, fonts
│ ├── page.tsx # home: heatmap + add form + habit list
│ ├── manifest.ts # web app manifest (Next 13+ convention)
│ ├── sw.ts # Serwist service worker source
│ ├── icon.svg # branded favicon (also favicon.ico + apple-icon.png)
│ └── globals.css # Tailwind v4 import + design tokens
├── components/ # flat list, co-located tests, no barrels
└── lib/ # Dexie client + pure helpers + hooks
public/
└── icons/ # PWA icons (192 / 512 / maskable)
tests/
└── e2e/ # Playwright: core flow, keyboard nav, a11y, offline
Key trade-offs documented in docs/DECISIONS.md as lightweight ADRs:
- Why Dexie (IndexedDB) instead of a backend
- Why Next.js for an offline-first app (and not Vite)
- Why D3 utilities instead of a chart library
- Why no global state library
- Why Tailwind v4
- Why pnpm over npm
Measured against the production build:
- Lighthouse: Performance 99 · Accessibility 100 · Best Practices 96 · SEO 100
@axe-core/playwright— 0 violations on both the empty and populated states (run in the E2E suite)- Installable on Chrome / Edge desktop and Android, via a custom deferred-prompt banner
- Works end-to-end in airplane mode — verified by reloading the production build offline; the service worker serves the shell and IndexedDB serves the data
Note: Lighthouse 12 retired the standalone PWA category, so installability is verified through the manifest + service worker + a working install prompt rather than a single score.
This is a portfolio demo. At production scale I would:
- Add a sync layer. Conflict-free replicated data types (CRDTs) or a server-backed sync to allow multi-device habit tracking. The current scope deliberately stops at single-device because that's where the "no server" constraint shines.
- Add notifications. PWA push needs VAPID keys, a server to send pushes from, and a service-worker push handler. The demo value is small for now; the cost of operating a notifications service is high.
- Move the heatmap to a worker if the dataset grew beyond a few years. For 365 cells × dozens of habits, the main thread is comfortable.
- Error monitoring (Sentry) and Web Vitals reporting.
Complete and deployed — all five modules shipped, live at demo-habit.jcortes.dev. PWA shell, Dexie data layer, core UI, the hand-built D3 heatmap, and the M5 polish (refined visuals, branded icons, demo data, install banner, E2E + a11y) are all in main. See docs/ARCHITECTURE.md for the full roadmap.
MIT — see LICENSE
Built by Josue Cortes as part of a frontend engineering portfolio. Find me on GitHub and LinkedIn.