Survival Budget

Living Interaction Design Specification

A privacy-first iOS budgeting app that organizes expenses into four priority tiers so you always know what to cut when money gets tight.

Platform

iOS (Capacitor)

Stack

React + Vite + Tailwind

Data

localStorage only

Updated

September 2026

System Specification

This living specification documents the interaction patterns, design constraints, and system decisions that govern how the product behaves.

It serves as the reference for design tokens, component patterns, and interaction rules used across the product.

Product Philosophy

The thinking behind the design decisions.

The Core Insight

Your budget builds from the ground up. Survival expenses, housing, food, utilities, are the foundation. Once those are covered, you move up through Important, Quality of Life, and Future Building. When money gets tight, you adjust from the top. The foundation stays protected.

Design Principles

Survival is non-negotiable

The foundation model. Survival gets funded before anything above it. When you need to adjust, you start from the top. The foundation is the last thing you touch.

No red, no shame

Brand-orange for warnings instead of red. The app never uses language or color that implies failure. Over-budget states say "Over Allocated", not "Over budget" in red. Financial anxiety is real; the UI shouldn't add to it.

Your data, your device

All data lives in localStorage. No server, no sync, no analytics, no tracking. The App Store privacy label says "Data Not Collected." Financial data is sensitive; the simplest security is not having it.

Permission to live

The Quality of Life tier exists because budgeting apps that only restrict spending are demoralizing. Netflix, a gym membership, a coffee habit. These aren't frivolous. They're permission to live while being responsible.

The Priority Pyramid

Rendered with the app's design tokens: the same fills, bar widths and icons as the onboarding welcome screen. Each tier narrows as it rises, so the base reads as the widest and most protected.

Future Building
Quality of Life
Important
Survival

Cover the base first. Adjust from the top.

Language Standard

Canonical phrasing for the allocation model. Applied consistently across the app UI, this spec, and all documentation.

Canonical One-Liner

"Cover the base first. Adjust from the top."

Used as the primary tagline for the allocation model in the app UI and this spec.

Structural Rule

The pyramid is structural, not directional. Survival sits at the base because it is the foundation: the widest bar, the last thing to cut. The system enforces:

  • Bottom-up allocation: funding starts at Survival and proceeds upward
  • Top-down reduction: when income falls short, Future Building is cut first

Approved Phrasing

  • "Cover the base first"
  • "Builds from the ground up"
  • "Foundation model"
  • "Allocation proceeds from lowest tier to highest"
  • "Adjust from the top"
  • "Cut from the top down"

Disallowed Phrasing

  • "Money flows top to bottom"
  • "Top-to-bottom" (as a directional description of allocation)
  • "Waterfall" (as the model name)
  • "Cut from the bottom up"

Spec Naming Convention

Model Name

Tiered Allocation Model

Not

Income Waterfall Calculation

In code, the variable remaining decrements per tier. This standard governs how the model is described, not how it executes.

Design Tokens

Every token rendered as a live specimen. Source of truth: tailwind.config.js + src/index.css

Colors

Brand Orange

#c94117

Primary (CTAs, brand)

#db4e1f

Light (gradients)

#b33710

Dark (hover states)

#fff7ed

orange-50 (backgrounds)

#ffedd5

orange-100 (subtle fills)

Priority Tiers

Survival

Icon #2563eb / bg #dbeafe

Important

Icon #c94117 / bg #ffedd5

Quality of Life

Icon #db2777 / bg #fce7f3

Future Building

Icon #16a34a / bg #dcfce7

Tier icons use the 600 shade on a 100 background (BudgetApp.tsx). Tier badges and labels use the 700 shade on the same background (ExpenseCategories.tsx).

Semantic

Success

#16a34a, 4.5:1

Warning

#ea580c, 4.5:1

Error

#dc2626, 4.5:1

Info

#2563eb, 4.5:1

Neutrals (WCAG contrast on white)

gray-900 #111827, 15.68:1, Primary text

gray-800 #1f2937, 12.63:1, Secondary text

gray-700 #374151, 9.73:1, Tertiary text

gray-600 #4b5563, 7.05:1, Muted text

gray-500 #6b7280, 4.93:1, Disabled text

gray-400 #9ca3af, 3.17:1, Borders (passes 3:1)

gray-300 #d1d5db, 1.85:1, Decorative only

Typography

Typeface

Inter, one family for every role

Headings, body, labels and figures all use Inter at 400, 500, 600 and 700. Hierarchy comes from weight, size and space rather than a second face.


Scale in use

3xl30px bold: Summary figures
26pxBold: Header screen title
2xl24px bold: Onboarding steps, status, empty states
xl20px bold: Section and dialog headings
lg18px semibold: Sheet and modal titles
base16px: Body text, form inputs
sm14px: Labels, descriptions, secondary text
xs12px: Helper text, metadata

Weights: font-medium for labels and body emphasis, font-semibold for titles and actions, font-bold with tracking-tight for headings and figures.

Spacing

4px space-1: spacing-xs

8px space-2: spacing-sm

16px space-4: spacing-md (content padding)

24px space-6: spacing-lg (card padding, component gap)

32px space-8: spacing-xl

48px space-12: spacing-2xl (touch targets)


Content width: max-w-[420px] · Content padding: px-4 py-3 · Card padding: p-6

Elevation (Shadows)

shadow-sm

Cards, inputs, content containers

shadow-md

Income card, summary hero, CTAs

shadow-lg

Modals, overlays, drawer

Borders & Dividers

border-black/5: Soft divider (cards, nav borders)

border-gray-400: Form borders (3.17:1 WCAG)

border-l-4 border-green-500: Covered

border-l-4 border-orange-500: Borderline

Touch Targets

48px

WCAG AAA

44px

iOS minimum

The Button component enforces min-h-[48px] min-w-[48px] on every variant, and nav buttons use the same 48px minimum height. Compact controls (icon buttons, toast actions, inline links) hold a 44px floor, the WCAG 2.5.5 target-size minimum. Nothing interactive ships below 44px. The header app icon is a fixed 36×36px image with rounded-lg (8px) corners (not a tap target).

Z-Index Scale

LayerZ-IndexUsage
Sticky UI10MobileHeader, MobileNavigation
Dropdowns20Goal card menus, popovers
Fixed notices30AppInstallBanner, OfflineIndicator
Backdrops40Modal/drawer backgrounds
Modals50BottomSheet, ConfirmationModal, Toast
Nested modals60AddMoneyModal (over other modals)
ExpenseModal9999Full-screen expense form
Skip link100Keyboard accessibility

Component Library

Live specimens rendered with the app's actual design tokens.

Button

Source: src/components/ui/Button.tsx · All variants enforce 48x48px minimum touch target.

Variants

Sizes

States

Full Width

Card Patterns

Standard Card

Card Title

Body text inside the standard card pattern.

bg-white rounded-2xl shadow-sm border border-black/5 p-6

Gradient Card (Income Hero)

Monthly Income

$4,500

Affordability Cards

Survival

Covered: $450 remaining

Important

Near Limit

Info Callout

Tip: Start with your survival expenses. Housing, food, utilities, transport.

Mobile Header

Source: src/components/Mobile/MobileHeader.tsx

With tagline (Home)

Survival Budget icon

Survival Budget

Priority-based budgeting

Without tagline (other screens)

Survival Budget icon

Settings

Header Structural Baseline

A) Header Top Baseline

  • Desktop web top inset: :root { --app-header-top: 1.5rem } (24px)
  • iOS standalone/fullscreen: safe-top uses max(44px, calc(env(safe-area-inset-top) + 16px))
  • Desktop web does NOT use a forced 44px minimum

B) Two-Line Reservation Rule

  • Header always reserves two text lines (title + tagline slot)
  • Title aligns to top of icon · Tagline aligns to bottom of icon
  • If tagline is absent: \u00A0 placeholder, aria-hidden, select-none
  • No conditional removal of the tagline element is allowed
  • Title uses leading-none to eliminate half-leading gap above the text glyph
  • Text wrapper uses min-h-[36px] to match icon height exactly

C) Optical Alignment

  • Outer row: items-start (top-aligns icon/text group with action buttons)
  • Inner left group: items-start (aligns icon top with title block top)
  • Icon: 36×36px with rounded-lg (8px corners)

D) Sticky Policy

  • Sticky: budget, categories, history
  • Non-sticky: home, savings, settings, privacy (default)

E) Chrome Rules

  • No border-bottom · No shadow · No backdrop blur
  • Background: opaque bg-white

bg-white safe-top · Conditionally sticky top-0 z-20 on budget/categories/history · No shadow, border, or blur

Bottom Navigation

Source: src/components/Mobile/MobileNavigation.tsx · 4 tabs, always visible.

Home
Budget
Savings
Settings

fixed bottom-0 left-0 right-0 bg-white border-t border-black/5 z-10 · Active: text-brand-orange · Inactive: text-gray-500

Empty State

Source: src/components/EmptyState.tsx · Pattern: icon circle + heading + supportive text + CTA + help text.

No savings goals yet

Set a goal and watch your progress grow. Whether it's an emergency fund or a vacation, we'll help you get there.

Tracking goals helps you stay motivated and see real progress.

Tone: never blaming, always suggesting next step.

Toast

"Netflix" deleted

Fixed bottom-20, z-50 · slide-up animation · auto-dismiss 5s · white underlined action on gray-800

Bottom Sheet

Source: src/components/Mobile/BottomSheet.tsx

Survival Expenses

Rent

$2,400

Groceries

$600

Drag handle + swipe-to-close (100px threshold) · createPortal to body · Backdrop z-40, sheet z-50 · Title border: border-gray-400

Expense Modal

Source: src/components/Mobile/ExpenseModal.tsx

Add Expense

Full-screen on mobile · createPortal to body · z-[9999] · focus trap · Escape to close

Toggle Switch

Enabled
Disabled

Interactive Prototype

A clickable phone frame using the app's real design tokens. Navigate with the bottom tabs or tap interactive elements.

9:41
Survival Budget icon

Survival Budget

Home

Screen Specifications

Each screen documented with purpose, content, interactions, and edge cases.

1

Onboarding (4 steps)

Purpose: First-run experience introducing the priority model and collecting survival data.

Step 1: Welcome

Priority pyramid visualization, "Survival is non-negotiable" headline, two benefits with checkmarks, Get Started (primary) and Skip to Budget (secondary) CTAs.

Step 2: Income & Survival Setup

Monthly income input, three survival expense fields (rent, groceries, utilities). All inputs enforce 16px minimum font to prevent iOS zoom.

Step 3: Budget Snapshot

Summary card showing entered data + calculated remaining. "Next" advances to Step 4, Widget Setup. Source: BudgetSnapshot.tsx. Sets onboardingCompleted = true in localStorage.

Step 4: Widget Setup

Home-screen widget instructions: long-press until icons jiggle, tap + or Edit in the top-left, search for "Survival Budget", choose a size and tap Add Widget. Source: WidgetSetupScreen.tsx.

Edge cases: Skip with empty data (income defaults to 0). User returns after partial onboarding (checked via localStorage flag). Skip triggers ConfirmationModal.

2

Home

Purpose: Landing screen for returning users, at-a-glance priority status.

Entry: Bottom nav "Home" tab (default after onboarding).

Content: Status card carrying the getSurvivalStatus() label and description, with Monthly income and Survival covered rows. Flexible-money card: "You have / $X / to work with". "After Quality of Life" card, labelled "Balanced" under $100 and "Available to save or enjoy" at or above it. "What's covered" list. Source: SummaryDashboard.tsx.

State variations: New user (no priorities) shows "Set your priorities" guidance card. Returning user (>5 min session gap) reloads from localStorage.

3

Budget (Core Screen)

Purpose: Primary workspace for priority-based expense allocation.

Content: Income gradient hero card (income / allocated / remaining). Four priority accordion cards (expandable). SpendingPlan callout.

SpendingPlan card: Persistent callout below priority cards. Line 1: "This month: Focus on Priorities {count}", count reflects tiers that have expenses entered AND are affordable (funded + affordable). Line 2 (conditional: shown only when income surplus > 0 and a next tier exists): "You have $X unallocated. Consider adding {Next Tier} expenses." Special case when count is 0 (no expenses entered): "Start by adding {Next Tier} expenses to build your plan."

Interactions: Toggle priority expand. Add expense (opens ExpenseModal). Adjust expenses (opens BottomSheet). Edit income (opens small BottomSheet). Toggle expense enabled/disabled. Swipe to edit/delete.

Calculation: Allocation proceeds from lowest tier to highest. Remaining = income minus sum of enabled expenses per tier. Reductions proceed from highest tier downward. Affordability: 100% = green (Covered), ≥50% = borderline orange (Near Limit), <50% = orange (Over Allocated).

Overlay management: Sheet closes before modal opens. Modal close returns to sheet via returnToSheetAfterModal flag (50ms delay).

4

Savings Goals

Purpose: Create and track savings goals with progress visualization.

Content: Add button (FAB, top-right). Summary gradient card (active/completed/total). Goal cards with progress bars.

Interactions: Add goal (AddGoalModal). Add money (AddMoneyModal, z-60). Edit goal. Delete goal with confirmation.

Budget sync: Saving or toggling a budget expense linked to a savings goal updates that goal's currentAmount in real time via updateSavingsGoalFromBudget() in BudgetApp.

Settings

Data export (JSON/CSV), Reset App (with ConfirmationModal), Privacy link, build version. Reset clears all localStorage and returns to onboarding.

Categories

Cross-cutting view of expenses by category. Access via hamburger menu only (not bottom nav). Groups across all priority tiers.

Budget History

View and restore past budget snapshots. Create snapshot, view details, restore (overwrites current). Hamburger menu only.

Privacy

Privacy policy for App Store compliance. Standalone at /privacy for web. In-app screen for native. Hamburger menu only.

Data Architecture

All data lives in localStorage. No server, no sync, no analytics.

Storage Keys

KeyTypePurpose
onboardingCompletedstringRouting gate (onboarding vs. home)
budgetDataJSONIncome + base survival expenses
lastSessiontimestampSession timeout detection (5 min)
budgetHistoryJSON[]Budget snapshots
savingsGoalsJSON[]Savings goals (flat SavingsGoal[] array)
appVersionstringData migration version (currently "1.0.0")
lastSnapshotDatestringDate of last budget history snapshot
budgetPrioritiesJSONPriority tiers with expenses (stored separately from budgetData)

Storage Shapes

Exact JSON shapes written to localStorage, verified against read/write boundaries in source.

budgetData

Write: BudgetApp.tsx · Read: BudgetApp.tsx, budgetHistory.ts

{ income: number, rent: number, groceries: number, utilities: number }

budgetPriorities

Write: BudgetApp.tsx · Read: BudgetApp.tsx, App.tsx, budgetHistory.ts

Priority[] // See Core Types below. Stored separately from budgetData

budgetHistory

Write: budgetHistory.ts · Read: budgetHistory.ts

type BudgetSnapshot = {
  id: string;
  date: string; // ISO month (YYYY-MM)
  timestamp: number;
  income: number;
  priorities: {
    id: number; name: string;
    totalExpenses: number; activeExpenses: number;
    expenseCount: number;
    isAffordable: boolean; isBorderline: boolean;
  }[];
  totalSpending: number;
  moneyLeft: number;
  survivalCovered: boolean;
  note?: string;
  isManual?: boolean;
}

type BudgetHistory = BudgetSnapshot[];

Core Types

type Priority = {
  id: number; // 1-4 (Survival → Future)
  name: string;
  expenses: ExpenseItem[];
  isAffordable: boolean;
  isBorderline: boolean;
}

type ExpenseItem = {
  id: string;
  name: string;
  amount: number;
  enabled: boolean;
  category?: ExpenseCategory;
  savingsGoalId?: string; // Links to SavingsGoal
  isRecurring?: boolean;
}

type SavingsGoal = {
  id: string;
  name: string;
  targetAmount: number;
  currentAmount: number;
  deadline?: string;
  category?: 'emergency' | 'purchase' | 'experience' | 'other';
  createdAt: string; // ISO timestamp
  updatedAt: string; // ISO timestamp
}

Tiered Allocation Calculation

Income is allocated from the base upward through the four priority tiers. At each level, the remaining budget determines affordability.

let remaining = income;

for (tier of [Survival, Important, QoL, Future]) {
  const tierCost = sum(tier.expenses.filter(e => e.enabled));

  if (remaining >= tierCost)
    tier.isAffordable = true; // Green: "Covered"
  else if (remaining >= tierCost * 0.5)
    tier.isBorderline = true; // Orange: "Near Limit"
  else
    // Orange: "Over Allocated"

  remaining -= tierCost;
}

Borderline threshold: 50% (from constants.ts AFFORDABILITY_THRESHOLD).

Interaction Patterns

Recurring patterns that appear across multiple screens.

Modal Pattern

All modals use createPortal(ui, document.body) for reliable layering on iOS Safari.

Backdrop

z-40, semi-transparent black

Content

z-50 (z-9999 for ExpenseModal)

Focus

Trapped within modal boundaries

Dismiss

Escape key, backdrop tap, close button

Overlay Conflict Resolution

When a modal needs to open from within a bottom sheet:

  1. Close the bottom sheet
  2. Set returnToSheetAfterModal = true
  3. Open the modal (50ms delay)
  4. On modal close: if flag is set, reopen sheet after 50ms

Haptic Feedback

Light

10ms

Navigation, toggles, add

Medium

15ms

Delete confirm, refresh

Heavy

[10,10,20]ms

Destructive actions

Graceful fallback: try/catch wrapper, no-op on unsupported devices.

Pull to Refresh

Threshold

60px pull distance

Resistance

0.3 factor

Spinner

Brand-orange RefreshCw icon

Delay

1.5s perceived loading

Available on: Home, Categories, History, Settings. Not on Budget or Savings (in-place editing).

Swipe to Reveal

Deadzone

8px before direction lock

Snap threshold

75px (150px max offset)

Actions revealed

Edit (blue) + Delete (orange)

Haptic

Feedback at snap threshold

Empty States

Pattern: icon circle + heading + supportive description + CTA button + help text.

Tone: Never blaming, always suggesting next step. Examples: "No income added yet. Even if it's uncertain, let's put something down." / "No savings goals yet. Set a goal and watch your progress grow."

Affordability Indicators

Covered: remaining ≥ tier cost

Near Limit: remaining ≥ 50% of tier cost

Over Allocated: remaining < 50% of tier cost

Rule: Affordability indicators never use red. The second and third states both use border-brand-orange, intentionally. No shame in the system. Red is reserved for destructive actions only (danger Button variant, e.g. "Reset App") and form validation errors.

Render: each priority card in BudgetApp.tsx takes its left border from priority.isAffordable ? 'border-green-500' : 'border-brand-orange'.

Decision Log

Architectural decisions with rationale. The "why" behind the product.

useState navigation over React Router

Context

Mobile app with 8 screens, deployed to iOS via Capacitor.

Decision

useState<Screen> with manual directional transitions.

Rationale

Simpler mental model. No URL routing needed for native. Enables directional slide transitions based on screen order index. Avoids Capacitor routing conflicts.

Trade-offs

No deep linking. No browser back button. Manual scroll restoration needed.

localStorage as sole data store

Context

Privacy-first budgeting app handling sensitive financial data.

Decision

All data in localStorage. No server, no sync, no analytics.

Rationale

Zero data exposure risk. Works offline. No server costs. App Store privacy compliance: "Data Not Collected." The simplest security is not having the data.

Trade-offs

No cross-device sync. Data loss on clear/uninstall. Limited storage (~5MB). Mitigated with JSON/CSV export in Settings.

4 fixed priority tiers (not custom categories)

Context

Needed a simple mental model for budget organization.

Decision

Survival > Important > Quality of Life > Future Building. Fixed, not configurable.

Rationale

Reduces decision fatigue. Creates a clear cut order: always adjust from the top down. Prevents analysis paralysis. Guides users toward sustainable budgeting without requiring financial literacy.

Trade-offs

Less flexibility, accepted deliberately. Tier names and their order are the same for every user, which keeps the cut order unambiguous even when someone would rank an expense differently.

Brand orange for warnings (never red)

Context

Budgeting apps create anxiety. Red implies failure and shame.

Decision

Use brand-orange (#c94117) for all warning/alert states. Never red in the budget UI.

Rationale

Orange signals "attention needed" without shame. Aligns with brand identity. Reduces financial anxiety: the app is a tool, not a judge.

Trade-offs

Reduced urgency signaling. May not match user expectations from banking apps. Red is reserved for danger variant buttons only (destructive actions like "Reset App").

createPortal for all overlays

Context

Fixed positioning inside scrollable containers is unreliable on iOS Safari.

Decision

All modals and sheets use createPortal(ui, document.body).

Rationale

Bypasses stacking context issues. Ensures overlays are truly viewport-fixed. Prevents iOS Safari scroll bugs where fixed elements move with page content.

Trade-offs

Need manual focus management. Z-index coordination across portalled elements. Overlay conflict resolution pattern required (sheet → modal → sheet).

Haptic feedback on every interaction

Context

Native iOS app needs premium tactile feel to compete with native apps.

Decision

Haptic trigger on all taps, toggles, swipes, and transitions.

Rationale

Bridges the gap between web and native feel. Provides confirmation without visual noise. Three intensity levels (light/medium/heavy) map to action significance.

Trade-offs

Minimal battery impact. Requires graceful fallback on web/unsupported devices.

50% borderline affordability threshold

Context

Binary affordable/not-affordable felt too harsh and uninformative.

Decision

Three states: affordable (100%), borderline (≥50%), needs adjustment (<50%).

Rationale

"Almost covered" is meaningfully different from "nowhere close." The borderline state acknowledges partial progress and encourages small adjustments rather than wholesale cuts.

Trade-offs

Additional complexity in UI messaging. Users need to understand three states instead of two.

Survival Budget: Living IXD Spec

Built by Articulate Digital · September 2026