Skip to content

ADR-0008: Mobile-First Web Design Principles

Last Updated: 2026-01-11 Status: Active Context: Decksmith


Context

Decksmith's web application must deliver a premium user experience on both mobile and desktop. With 60%+ of web traffic coming from mobile devices, mobile cannot be an afterthought—it must be a first-class citizen.

Key challenges:

  1. Touch interactions: Deck building requires precise interactions (adding cards, adjusting quantities, organizing)—must work seamlessly with touch on small screens
  2. Screen real estate: Complex features (deck editor, card search with filters, print sheet builder) must adapt to 320px mobile screens without sacrificing usability
  3. Performance: Mobile devices have slower networks and less processing power—image loading and API responses must be optimized
  4. Future cross-platform: A React Native mobile app is planned—web architecture must support shared logic without duplication

Critical constraint from CLAUDE.md:

  • Domain logic only in packages/domain (pure functions, no I/O)
  • apps/web cannot use db, domain, scryfall, pdf (client-server boundary)

This ADR defines responsive breakpoints, touch interaction standards, offline strategy, and performance targets for mobile web.


Current Decision

We adopt a mobile-first responsive design strategy for the web application with the following principles:

1. Responsive Breakpoints

Three-tier breakpoint system:

BreakpointRangeTarget DevicesLayout Strategy
Mobile320-767pxPhonesSingle column, full-screen modals, bottom sheets
Tablet768-1023pxTablets, small laptopsTwo-column layouts, slide-over panels
Desktop1024px+Laptops, monitorsMulti-column, sidebars, drag-and-drop

CSS approach:

css
/* Mobile-first: Base styles target 320px+ */
.card-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr); /* 2 columns on mobile */
  gap: 0.5rem;
}

/* Tablet: Add complexity */
@media (min-width: 768px) {
  .card-grid {
    grid-template-columns: repeat(3, 1fr); /* 3 columns */
    gap: 1rem;
  }
}

/* Desktop: Full layout */
@media (min-width: 1024px) {
  .card-grid {
    grid-template-columns: repeat(4, 1fr); /* 4 columns */
    gap: 1.5rem;
  }
}

Rationale: Mobile-first CSS forces us to design for constraints first, then progressively enhance for larger screens (align with "Clarity over cleverness").

2. Touch Interaction Standards

Minimum touch target sizes (WCAG AAA compliance):

  • Buttons/links: 44px × 44px minimum (iOS/Android standard)
  • Form inputs: 44px height minimum (prevents iOS zoom on focus)
  • Swipeable elements: 48px height (comfortable swipe gesture)
  • Base font size: 16px (prevents iOS auto-zoom on input focus)

Touch gesture patterns:

ActionDesktopMobile Web
Add card to deckClickTap card → bottom sheet "Add to Deck"
Remove cardHover → click XSwipe left → delete button
Edit quantityInline number inputTap → modal with +/- buttons (large touch targets)
View card detailsClick thumbnailTap thumbnail → full-screen modal
Navigate filtersSidebar checkboxesBottom sheet with grouped filters

Rationale: Touch gestures must feel natural (swipe-to-delete is iOS/Android standard). Avoid hover-dependent interactions on mobile (align with "Premium UX" requirement).

3. Offline Strategy

Web application (mobile + desktop): Online-only initially

Decision:

  • Web app requires internet connection
  • No offline deck building or card browsing on web
  • API calls fail gracefully with error messages ("No internet connection")

Rationale:

  1. Reduces complexity: Offline-first requires sync strategy, conflict resolution, local database (IndexedDB), cache invalidation—significant engineering overhead
  2. User expectations: Users expect web apps to require internet (unlike native mobile apps)
  3. MVP focus: Online-only gets us to market faster without sacrificing core functionality
  4. Future-proof architecture: packages/domain is designed offline-ready (pure functions, no I/O), so offline support can be added later without refactoring

Domain layer design for future offline support:

typescript
// ✅ GOOD: Pure function in packages/domain
export function validateDeckFormat(deck: DeckDTO, format: FormatDTO): ValidationResult {
  // Pure logic, no API calls, no database
  // Works offline if given deck + format data
  return {
    isValid: deck.cards.every((card) => format.legalCards.includes(card.id)),
    errors: [
      /* ... */
    ],
  };
}

// ❌ BAD: Impure function (API call)
export async function validateDeckFormat(deckId: string): Promise<ValidationResult> {
  const deck = await fetch(`/api/decks/${deckId}`); // ❌ I/O in domain
  // ...
}

Key principle: Domain logic operates on DTOs from packages/schema, never directly on API responses or database models (align with ADR-0005: Package Boundaries).

Future native mobile: Will implement full offline-first with local database (SQLite), background sync, conflict resolution (spec'd in future ADR).

4. Performance Targets

Mobile web performance targets:

MetricTargetRationale
Initial page load< 2s on 4GFirst impression is critical
API response time< 500ms (p95)Perceived as instant
Card search results< 300msReal-time feel
Image loadingProgressive (thumbnail → full)Avoid layout shift, save bandwidth
Deck list render< 100ms for 60 cardsSmooth scrolling

Image optimization strategy:

typescript
// Use Scryfall's multiple image sizes
interface CardImageDTO {
  small: string    // ~146x204px (~10 KB) - Thumbnail
  normal: string   // ~488x680px (~100 KB) - Default
  large: string    // ~672x936px (~200 KB) - High-res
  png: string      // ~1500x2100px (~1 MB) - Print quality
}

// Mobile web: Load small first, upgrade to normal on tap
<img
  src={card.imageUris.small}
  data-full-src={card.imageUris.normal}
  loading="lazy" // Native lazy loading
  onClick={loadFullImage}
/>

Lazy loading:

  • Use Intersection Observer API for card grids (load images as user scrolls)
  • Virtual scrolling for large collections (1000+ cards) with react-window or @tanstack/react-virtual

Debouncing:

  • Search input: 300ms debounce (avoid API spam)
  • Filter changes: 500ms debounce (batch multiple filter changes)

Rationale: Mobile networks are slower and more expensive (align with "Performance targets" in specs).

5. Navigation & Routing

Same routes across mobile/desktop (no separate mobile routes):

/decks              → Deck list
/decks/:id          → View deck
/decks/:id/edit     → Edit deck
/cards              → Card search
/collection         → Collection management
/print              → Print sheet builder

Responsive UI patterns (same route, different layout):

RouteDesktopMobile Web
/decksTable with columnsCard list with summaries
/decks/:idSidebar + main contentFull-screen with bottom tabs
/cardsSidebar filters + gridFull-screen search with sheet filters
/printVisual builder (drag-and-drop)Text-based "print job creator"

Rationale: Same routes simplify mental model, improve SEO, enable deep linking for future native app (align with "Clarity over cleverness").


Rationale

Why Mobile-First Fits Decksmith's Values

  1. Separation of concerns:

    • Responsive CSS in packages/web-ui (presentation layer)
    • Domain logic in packages/domain (platform-agnostic)
    • Clear boundary between layout and logic
  2. Deterministic behavior:

    • Pure functions in domain layer work identically on mobile/desktop/native
    • No device-specific business logic (only UI variations)
  3. Minimal coupling:

    • Mobile web and desktop web share same components (packages/web-ui)
    • Future native app shares domain logic (packages/domain) and DTOs (packages/schema)
    • No duplication of validation, calculations, or business rules
  4. Clarity over cleverness:

    • Mobile-first CSS is simpler to reason about (base → enhancement, not overrides)
    • Touch interactions follow platform conventions (iOS/Android patterns)
    • Progressive image loading is standard web practice (not custom solution)

Why Online-Only for Web Initially

Offline-first is complex:

  • Requires local database (IndexedDB or WebSQL)
  • Sync strategy: Optimistic UI updates, conflict resolution, queue failed mutations
  • Cache invalidation: When to fetch fresh data vs use stale cache
  • Storage limits: Browsers have quotas (50MB-1GB varies by browser)

Offline-first is most valuable on native mobile:

  • Users expect native apps to work offline (subway, airplane)
  • React Native has better offline primitives (SQLite, AsyncStorage)
  • Longer sessions (building decks on commute, at tournament)

Web users expect internet:

  • Web apps typically require connection (Gmail, Spotify web, etc.)
  • Offline web is still "nice to have," not expected

Verdict: Defer offline to native mobile, keep web online-only for MVP (can add later if user feedback demands it).

Why These Breakpoints

320px minimum:

  • iPhone SE (375px) and older Android phones (360px) are smallest common devices
  • Supporting 320px ensures compatibility with nearly all mobile devices

768px tablet threshold:

  • Standard breakpoint (iPad portrait is 768px)
  • Matches Tailwind's md breakpoint (developer familiarity)

1024px desktop threshold:

  • Most laptops are 1280px+ width (comfortable for multi-column layouts)
  • Matches Tailwind's lg breakpoint

Trade-offs

Benefits:

  • Premium mobile UX: 60%+ of users get first-class experience (not "mobile version")
  • Future-proof architecture: Domain layer ready for offline support (pure functions)
  • Cross-platform code reuse: Domain logic + DTOs shared between web and future native app
  • Accessibility: 44px touch targets meet WCAG AAA standards (align with "Premium UX")
  • Performance: Progressive image loading saves bandwidth on mobile networks
  • SEO: Same routes for mobile/desktop improve search rankings (no /m/ subdomain)

Costs:

  • Design complexity: Must design every feature for mobile, tablet, desktop (3x layout work)
  • CSS maintenance: Responsive CSS requires careful breakpoint management
  • Testing overhead: Must test on multiple devices (iPhone, Android, tablet, desktop)
  • No offline on web: Users without internet cannot use app (acceptable for MVP)

Risks:

  • Feature creep: Designers may want different features on mobile vs desktop (violates "same routes" principle)
    • Mitigation: Feature parity matrix (ADR-0009) defines what's available where
  • Performance regressions: Mobile performance targets may be missed without monitoring
    • Mitigation: Add performance budgets to CI (Lighthouse CI, bundle size limits)
  • Touch interaction bugs: Gestures may conflict (swipe-to-delete vs horizontal scroll)
    • Mitigation: Use battle-tested libraries (react-swipeable, framer-motion)

Evolution History

2026-01-11: Initial decision

  • Defined mobile-first responsive design strategy (320px, 768px, 1024px breakpoints)
  • Established touch interaction standards (44px minimum, WCAG AAA)
  • Decided online-only for web initially (offline deferred to native mobile)
  • Designed domain layer for future offline support (pure functions, no I/O)
  • Set performance targets (< 2s page load, < 500ms API, progressive images)

References

Built with VitePress