Streak Habits - Product Requirements Document
1) Summary
Streak Habits is a phone-first web app for one person who wants to build daily habits. The user creates up to 12 habits, taps once per habit each day to mark it done, and sees a current streak (consecutive local calendar days completed) and a best streak beside each habit. All data lives in the browser on the user's own device; there are no accounts and no server.
- Assumption: Storage is local only (IndexedDB, single user, single device). Clearing browser data erases habits; there is no sync or export.
- Assumption: "Daily streak" means the number of consecutive local calendar days, ending today or yesterday, on which the habit was completed, not 24-hour windows from the last tap.
- Assumption: A day runs midnight to midnight in the device's local time zone; dates are stored as
YYYY-MM-DDstrings. - Assumption: Caps: name 1-40 characters, at most 12 active habits, completions stored for the last 730 days per habit.
- Assumption: Core formula: the streak counts back from today if today is done, otherwise from yesterday; see 4 for pseudo-code.
- Assumption: Today is never a broken day. A habit not yet done today keeps its streak until midnight passes without a tap.
- Assumption: Platform is a browser on phone and desktop, installable as a PWA so it opens like an app and works offline.
- Assumption: Nothing is automatic: no reminders or notifications, no backfilling the future. Past days within 7 days can be corrected by hand.
2) Tech Stack
- Framework → React 18 + Vite → fast static build, no server needed
- Language → TypeScript → typed habit and completion records
- Storage → IndexedDB via
idb-keyval→ simple persistent key-value store - Auth → None → single user, local data
- Styling → Tailwind CSS → quick mobile-first layout
- Dates →
date-fns→ formatting and local calendar math - PWA →
vite-plugin-pwa→ installable and offline - Deployment → Netlify static hosting → one route, no fallback rule needed
3) Users & Screens
Single user on one device; no accounts.
Screens:
| Screen | Reached from | Shows | Empty / loading / error | Back / close goes to |
|---|---|---|---|---|
| Today | App launch | Date heading, progress line, habit rows with check button, streak, best | Empty: No habits yet. Add your first one. with Add habit button. Loading: blank surface under 300 ms, no spinner. Error: see 6.1 | None (home) |
| Add/Edit Habit sheet | + Add habit button, row Edit | Name field, color swatches, Save, Delete (edit only) | Validation errors inline, see 6.1 | Today |
| Habit Detail | Tap habit name | Streaks, 30-day grid, 7-day correction row | Empty grid cells for days before creation: muted | Today |
| Delete confirm modal | Delete in edit sheet | Warning and two buttons | None | Edit sheet |
Navigation: single screen app; Today is home, and the others open as sheets or a pushed detail view with a ← Back button.
4) Data Model & Logic
Habit
id: string (UUID, example"a1f3…")name: string (1-40 chars, trimmed, example"Read 20 pages")color: string (hex from palette, default#2563EB)createdOn: string (YYYY-MM-DD, local date of creation)order: number (0-based position, default append)
Completion
habitId: stringdate: string (YYYY-MM-DD)- Stored as a set of date strings per habit under key
done:{habitId}.
Uniqueness, sort and retention: one completion per habit per date; habits sort by order ascending; completions older than 730 days are pruned on app open.
Logic:
today = localDate(now)
isDone(h, d) = done[h].has(d)
currentStreak(h):
start = isDone(h, today) ? today : addDays(today, -1)
n = 0
d = start
while d >= h.createdOn and isDone(h, d):
n += 1; d = addDays(d, -1)
return n
bestStreak(h):
walk sorted completion dates; run = 1 while
daysBetween(prev, cur) == 1, else run resets to 1;
return max(run) (0 if no completions)
daysBetween(a, b) = round((Date.UTC(b.y,b.m,b.d) − Date.UTC(a.y,a.m,a.d)) / 86400000)
Worked example: Habit "Read" created 2025-03-01. Completions: Mar 1, 2, 3, 4, 6, 7. Today is Mar 7 (done today).
currentStreak: start Mar 7 done, Mar 6 done, Mar 5 not done → stop. Streak =2.bestStreak: Mar 1-4 run is 4, then Mar 6-7 run is 2 → best4.- Midnight crossing: at 23:59 on Mar 7 the habit "Meditate" has completions Mar 5, 6 only and today not done.
currentStreakstarts at yesterday (Mar 6): Mar 6 done, Mar 5 done, Mar 4 not →2, still alive. At 00:01 on Mar 8 with no tap on Mar 7, start = Mar 7, not done →0: the streak broke at midnight. - Third record, "Stretch": completions Mar 7 only, created Mar 7 → current
1, best1.
Status transitions: each habit's day state:
| From | Event | To | Side effect |
|---|---|---|---|
pending (today, not done) | Tap check | done | Add date, recompute streaks, pulse animation |
done | Tap check again | pending | Remove date, recompute (undo) |
pending | Midnight passes | missed | None stored; streak recomputes to 0 |
missed (past day, within 7 days) | Tap in correction row | done | Add date, recompute |
done (past day, within 7 days) | Tap in correction row | missed | Remove date, recompute |
missed older than 7 days is terminal (read-only).
Edits and edge cases: renaming never touches completions. Deleting a habit removes its completions. Editing a past day recomputes both streaks immediately. A habit created today starts at 0 with today pending. Device clock moved backward: dates after today are ignored in streak math. At 12 habits the Add habit button is disabled.
5) External Services
None. All data is local and no third-party API is called.
6) Core Features
6.1 Add and Edit Habits
User Flow:
- On Today, user taps
+ Add habit; the sheet opens with the headingNew habit. - User types a name and taps one of 8 color swatches (default first swatch).
- User taps
Save; the habit is stored withcreatedOn= today and appears at the bottom of the list. - To edit, user taps
Editon a row; the sheet opens with headingEdit habitand theDeletebutton. Deleteopens the confirm modal:Delete "{name}" and its history? This cannot be undone.withCancelandDelete.
UI Copy & Colors:
- Add button:
+ Add habit, text#FFFFFFon#2563EB - Name placeholder:
e.g. Read 20 pages, text#64748B - Save:
Save,#FFFFFFon#2563EB; Delete:Delete,#FFFFFFon#DC2626
Error & Empty States:
- Name empty →
Give your habit a name.Recovery: type a name. - Name over 40 chars → input stops accepting characters and counter shows
40/40. - Duplicate name (case-insensitive) →
You already track "{name}".Recovery: change the name. - 12 habits exist → button disabled, helper
You can track up to 12 habits.Recovery: delete one. - Storage write fails → banner
Couldn't save. Check browser storage and try again.Recovery: retrySave.
Acceptance Checks:
- ✓ Name
Run→Save→ rowRunappears with streak0and best0. - ✓ 41 typed characters → field holds 40.
- ✓ Name
runwhenRunexists →You already track "run". - ✓ Delete confirmed → habit and its completions disappear; Today shows one fewer row.
6.2 Mark Done Today
User Flow:
- Today shows each habit row: color dot, name, a 44×44 px circular check button, and
🔥 {n}current streak withBest {m}below. - User taps the check button; it fills with the habit color and shows
✓. - The streak number updates instantly; if it equals or passes best, best updates too.
- Tapping a filled button un-marks today (undo).
- The header progress line reads
{done} of {total} done today; at all done it readsAll done today. Nice work!
UI Copy & Colors:
- Done button:
✓#FFFFFFon habit color; pending: border#CBD5E1, fill#FFFFFF - Streak:
🔥 {n}text#0F172A; zero streak shows🔥 0in#64748B - Progress line: text
#64748B; all-done text#16A34A
Error & Empty States:
- Write fails → button reverts and toast
Couldn't save that tap.Recovery: tap again. - No habits → empty state per section 3.
Acceptance Checks:
- ✓ Streak
2(Mar 6, 7 pending on Mar 8 → shown as2before tapping) → tap on Mar 8 →3. - ✓ Tap then tap again on the same habit → returns to
pending, streak back to prior value. - ✓ 3 habits, 2 done →
2 of 3 done today. - ✓ All 3 done →
All done today. Nice work!
6.3 Habit Detail and Corrections
User Flow:
- User taps a habit name; Habit Detail opens with
← Back. - The top shows
Current streakandBest streakas large numbers. - A 30-day grid (6 columns × 5 rows, oldest top-left, today bottom-right) fills done days with the habit color and missed days with
#E2E8F0; days beforecreatedOnare#F1F5F9and not tappable. - Below, the correction row
Last 7 daysshows 7 labelled day chips (Mon 3); tapping a chip toggles done/missed for that day. - Totals line:
{n} days completed since {createdOn formatted d MMM yyyy}.
UI Copy & Colors:
- Labels:
Current streak,Best streak, text#64748B; numbers#0F172Aat 32 px - Done chip:
#FFFFFFon habit color; missed chip:#0F172Aon#E2E8F0
Error & Empty States:
- New habit with no history → grid all muted, totals
0 days completed since {date}. - Tapping a day before
createdOn→ nothing happens; chip is dimmed witharia-disabled.
Acceptance Checks:
- ✓ Completions Mar 1-4, 6, 7 viewed on Mar 7 → current
2, best4. - ✓ Toggle Mar 5 to done → current
7, best7. - ✓ Toggle Mar 5 back → current
2, best4. - ✓ Habit created today → 6 grid cells before today are
#F1F5F9.
6.4 Midnight Rollover and Offline Use
User Flow:
- On
visibilitychangeto visible and every 60 s while open, the app recomputestoday. - If the date changed, all check buttons reset to
pendingand streaks recompute (see 4). - When offline, the app loads from the service worker cache and works unchanged.
UI Copy & Colors:
- Date heading:
EEEE, d MMMM(e.g.Saturday, 8 March), text#0F172A
Error & Empty States:
- IndexedDB unavailable (private mode) → full-screen
Your browser is blocking local storage, so habits can't be saved. Open this page in a normal window.Recovery: reopen outside private mode.
Acceptance Checks:
- ✓ Open at 23:59 with all done, return at 00:01 → heading shows the new date and
0 of {n} done today. - ✓ Habit with Mar 6 done, nothing Mar 7: at 00:01 Mar 8 shows
🔥 0; at 23:00 on Mar 7 still shows🔥 1. - ✓ Airplane mode after first load → app opens and taps still save.
7) UI/UX
Palette:
| Token | Hex | Used for |
|---|---|---|
| primary | #2563EB | Buttons, default habit color |
| success | #16A34A | All-done message |
| warning | #F59E0B | Banner background tint |
| danger | #DC2626 | Delete |
| text | #0F172A | Headings, numbers |
| muted | #64748B | Captions, zero streak |
| surface | #F8FAFC | Page background |
| border | #CBD5E1 | Pending check ring |
| missed | #E2E8F0 | Missed grid cells |
Habit swatches: #2563EB, #16A34A, #F59E0B, #DC2626, #9333EA, #0891B2, #DB2777, #475569.
Typography: Inter, 16 px base; page heading 24 px bold; streak and detail numbers 32 px bold; captions 13 px.
Layout: single column, mobile first, max width 480 px centred; the habit list is the hero, with the progress line pinned under the date heading.
Motion:
| Interaction | Duration | Easing |
|---|---|---|
| Check fill | 150 ms | ease-out |
| Streak number change | 200 ms | ease-in-out |
| Sheet open/close | 250 ms | ease-out |
| Toast in/out | 200 ms | ease |
Timestamp display: createdOn shown as d MMM yyyy; heading as EEEE, d MMMM; day chips as EEE d.
Accessibility: text contrast ≥ 4.5:1, tap targets ≥ 44×44 px, visible focus ring #2563EB 2 px, check buttons have aria-label="Mark {name} done" and aria-pressed.
8) Infrastructure Notes
Environment variables: None.
Client vs server: everything runs in the browser; there is no server code.
Platform limits:
| Limit | Platforms affected | Design answer |
|---|---|---|
| Browser storage can be cleared, and Safari may evict data of sites unused for 7 days | Safari iOS/macOS | Call navigator.storage.persist() on first save; recommend installing to Home Screen in the empty state footer text Tip: add to Home Screen to keep your data safe. |
| No background timers while closed | All | Midnight handled by recompute on open/visible, see 6.4 |
| Private mode may block IndexedDB | Safari, Firefox | Blocking message in 6.4 |
9) Out of Scope (V1)
- Reminders and push notifications - tempting, but they need a service worker push setup and a server.
- Weekly or custom schedules (e.g. 3x per week) - tempting, but streak logic changes entirely; V1 is daily only.
- Cloud sync and accounts - requires a backend and auth, against the local-only design.
- Statistics and charts - monthly trends and completion rates go beyond the 30-day grid.
- Streak freezes or skip days - changes the streak rule defined in 4.
- Data export and import - useful, but not needed for a first version.
- Dark mode and themes - extra palette work not required by the pitch.
10) Success Criteria
- ✓ Adding a habit named
Readshows a row with🔥 0andBest 0. - ✓ Tapping the check button fills it and updates the streak to
1on a new habit. - ✓ Tapping the filled button again returns it to pending and streak to
0. - ✓ A habit with Mar 6 and Mar 7 done shows streak
2on Mar 7 and Mar 8 (before a tap on Mar 8). - ✓ A missed full day resets the streak to
0at the next midnight. - ✓ Best streak never decreases when a streak breaks.
- ✓ A 13th habit cannot be added and
You can track up to 12 habits.appears. - ✓ Deleting a habit asks for confirmation and removes its history.
- ✓ Toggling a day within the last 7 days in Habit Detail recomputes current and best streaks immediately.
- ✓ No console errors during adding a habit, marking it done, and reopening the app after a reload.