Building a Habit Tracker with Next.js 16, Tailwind v4 & shadcn/ui
A deep dive into building a mobile-first habit tracker with modern Next.js, shadcn/ui components, framer-motion animations, and a localStorage-first architecture for offline-first personal tracking.
Building a Habit Tracker with Next.js 16, Tailwind v4 & shadcn/ui
I've always struggled with consistency. Whether it's coding, exercising, or reading — I start strong but fizzle out after a week. So I built a habit tracker to solve my own problem, deployed it locally with PM2, and iterated through several architectural decisions along the way.
Why Build Your Own vs Buying?
There are plenty of habit tracking apps out there (Streaks, Habitica, Loop). But I wanted something that:
- Zero friction — No sign-up, no cloud sync, no permissions. Just open and use.
- Looks good enough — I'd actually want to open it daily on my phone.
- Streaks-first — Focus on consecutive days, not analytics or complex scoring.
- Offline-first — Works anywhere, anytime, no API keys or subscriptions.
- Own my data — Everything stays in browser localStorage, exportable JSON.
The Stack (Production)
After a few iterations, this is the current stack on main:
| Layer | Tech |
|---|---|
| Framework | Next.js 16.2 (App Router, React 19) |
| Styling | Tailwind CSS v4 + shadcn/ui (Radix primitives) |
| State | React Query (TanStack) + custom useStorage hook |
| Auth | — (localStorage only for now) |
| DB | — (localStorage only for now) |
| UI Polish | Framer Motion (page transitions, card animations) |
| Icons | Lucide React |
| Notifications | Sonner (toast) |
| Deployment | PM2 on VPS (port 3002), Nginx reverse-proxy-ready |
The app is deliberately client-only (data never leaves the browser). No database, no API routes. All state lives in localStorage.
Architecture & Data Model
Types (TypeScript-first)
tsLoading...Loading syntax highlighting...
Data is stored under two keys in localStorage:
ht_habits— array of Habit objectsht_logs— nested logs object keyed by date
Custom Hook: useStorage
I built a reusable hook that wraps localStorage with automatic JSON serialization + cross-tab sync via CustomEvent:
tsLoading...Loading syntax highlighting...
This means if you open the app in two browser tabs, checking a habit in one instantly reflects in the other. No React Query needed for this — localStorage events are enough.
Features
1. Streak Calculation (Server-side Friendly)
The getStreak() utility walks backwards from today through the logs to count consecutive days. It respects the habit's frequency (daily | weekdays | weekends) so a "weekdays" habit only counts Monday–Friday.
2. Daily Dashboard (/today)
All habits for today in one scrollable list. Each card shows:
- Emoji icon with custom background (tinted from habit.color)
- Habit name + frequency badge
- Current streak badge (🔥 12)
- Animated checkmark button (Framer Motion)
- Notes modal (incomplete — TODO)
- Edit / Delete via dropdown (shadcn/ui DropdownMenu)
Check off a habit → instant localStorage write + streak recalc + toast notification.
3. Weekly Overview (/history)
Calendar-style grid (7 columns × N weeks). Green squares = completed, gray = missed. Built with:
- Tailwind grid +
date-fnsfor date math - Each cell is a clickable pill that opens the
/todayview for that date
4. Habits Management (/habits)
CRUD for habits via HabitModal (Dialog + Form). Features:
- Color picker (circular swatches, 8 preset colors)
- Emoji picker (uses
emoji-picker-react) - Frequency selector (RadioGroup)
- ZOD validation on save
5. Settings (/settings)
- Export data → downloads JSON backup
- Import data → replaces current state
- Clear all data (with confirmation)
UI/UX Details
shadcn/ui Everywhere
Instead of hand-rolling dialogs, dropdowns, toasts — I used Radix-based shadcn primitives:
Dialog,AlertDialogfor modalsDropdownMenufor habit actionsButton,Textarea,Labelfor formsSonnerfor toasts (positioned top-right)
All components are unstyled by default, styled via Tailwind utility classes. This keeps bundle size low and theming consistent.
Framer Motion Polish
- Cards animate on mount (slide + fade in)
- Checked habits get a subtle gradient shine sweep (
.animate-shine) - Modal open/close transitions (scale + fade)
- Staggered list animations (cascade effect)
Dark Mode via next-themes
Switches between light/dark system preference. All colors use Tailwind's dark: variants (grays, zinc, slate).
Lessons Learned (Technical)
1. Turbopack Panic on Build (Next.js 16)
After upgrading to Next.js 16.0.10, next build crashed with:
Turbopack Error: Dependency tracking is disabled so invalidation is not allowed
Root cause: Turbopack's new default behavior in Next.js 16 conflicted with the workspace root detection (multiple package-lock.json files). Fix:
- Upgraded to Next.js 16.2.4 (patch includes Turbopack stability fixes)
- Deleted conflicting route folder (
app/sitemap.xml/) that collided withapp/sitemap.ts - Cleared
.nextcache before rebuild
2. SSR localStorage is not defined
During static generation, any direct localStorage access throws. Solutions:
- Wrap all localStorage calls in
useEffectoruseStorage(which initializes lazily viauseStategetter) - Add
'use client'directives to all components using localStorage - Home page uses
mountedflag to avoid hydration mismatch
3. Next.js 16 Strict Route + Metadata Conflicts
Having both app/sitemap.ts (route) and app/sitemap.xml/ (folder) simultaneously triggers:
Conflicting route and metadata at /sitemap.xml
Removed the folder, kept the file route.
4. Feature-based Folder Structure
app/
habits/ → CRUD + list
today/ → daily check-in
history/ → weekly grid
settings/ → import/export
layout.tsx → Providers, metadata
page.tsx → redirect to /today if habits exist, else onboarding
components/
habit-card.tsx
habit-modal.tsx
delete-modal.tsx
streak-badge.tsx
check-mark.tsx
onboarding.tsx
lib/
storage.ts ← localStorage hook (cross-tab sync)
streaks.ts ← getStreak() math
types.ts ← TypeScript interfaces
utils.ts ← cn() helper
This keeps concerns separated: UI components, business logic, and data storage don't mix.
5. Why Not Use React Query for LocalStorage?
I initially thought about using React Query to cache localStorage reads. But:
- localStorage is synchronous and instant — no need for loading states
- Cross-tab sync via
CustomEventis lighter than Query's cache invalidation - Simpler to just use a custom hook — fewer dependencies, no query keys to manage
If I ever add a backend sync layer, I'll reintroduce React Query as a caching layer between localStorage and the network.
What's Next (Roadmap)
- Data Export/Import — Already in settings (JSON blob). Could add iCal/CSV export.
- Widget / PWA — Manifest + service worker for installability on home screen.
- Reminders — Since localStorage has no push, I'd need a background service worker (complex).
- Backend Sync — Supabase or a simple Next.js API route to sync across devices. Would let me keep React Query.
- Gamification — Add XP per completion, level-ups, maybe a leaderboard (local only).
- Chrome Extension — Quick-check from toolbar.
Deployment Notes
Production build runs on PM2 as a fork process:
bashLoading...Loading syntax highlighting...
Nginx reverse-proxy on my VPS routes:
habittracker.example.com→localhost:3002
SSL: Let's Encrypt via DNS-01 challenge (domain uses Cloudflare with Basic Auth protection on subdomains).
Source Code
The repo is open (private by choice) but the architecture serves as a template for other client-only apps. Key takeaway: You don't need a backend to build something useful. localStorage is surprisingly powerful when paired with good UX.
The best productivity app is the one you actually use. This one, I use daily.
Related Posts
Why I Rebuilt My Portfolio
My old portfolio was a mess. Here's how I rebuilt it with Next.js 15, Tailwind v4, and a feature-based architecture.
Read moreStop Paying for Notes — Obsidian + Syncthing Is All You Need
A free, private, cross-platform note-taking system using Obsidian and Syncthing — with setup guides for macOS, Windows, and Android. No subscriptions, no cloud, no compromises.
Read moreSetting Up Hermes Agent — My AI-Powered Dev Assistant
How I set up Hermes Agent with 40+ custom skills, RTK token optimization, and automated workflows for finance, development, and productivity.
Read more