Back to System Design
System Design
easy
mid

How would you design a phone style web app with multiple screens and apps (low level design)?

Lock screen → home with app grid → app screens; each app a self-contained route. State: current app (route), back-stack, modal layer, notifications. App registry pattern: each app declares its icon, name, route, and entry component. Status bar + dock fixed; gesture/keyboard nav between screens. Lazy-load each app's bundle.

5 min read·~40 min to think through

A phone-screen-style web app simulates iOS / Android — lock screen, home with apps, individual apps with their own state. It's a routing + composition exercise.

1. Top-level structure

ts
<Phone>
<StatusBar /> (time, battery, signal — fixed top)
<ScreenStack>
  │   ├ <LockScreen />            (initial)
  │   ├ <HomeScreen apps={apps} onOpen={openApp} />
  │   └ <AppScreen current={openAppId} />
<Dock />                     (fixed bottom)
<NotificationLayer />

2. App registry

Each app is a record:

js
const apps = [
  { id: "messages", name: "Messages", icon: MessageIcon, Component: lazy(() => import("./apps/Messages")) },
  { id: "photos", name: "Photos", icon: PhotoIcon, Component: lazy(() => import("./apps/Photos")) },
  // ...
];
  • Registering an app = adding one entry.
  • Apps are lazy-loaded so the initial bundle is small.

3. Navigation state

js
const [route, setRoute] = useState({ stack: ["home"] });

const openApp = (id) => setRoute({ stack: [...route.stack, id] });
const goBack = () => setRoute({ stack: route.stack.slice(0, -1) });

The current route is the top of the stack. Open app pushes; back pops.

4. Lock screen

jsx
{!unlocked ? (
  <LockScreen onUnlock={() => setUnlocked(true)} />
) : (
  <HomeScreen ... />
)}

PIN / pattern / biometric simulation; transitions to home on success.

5. Home grid

jsx
<div className="grid">
  {apps.map((app) => (
    <button key={app.id} onClick={() => openApp(app.id)}>
      <app.icon />
      <span>{app.name}</span>
    </button>
  ))}
</div>

CSS grid; tap target ~60px+; accessibility labels.

6. App screen rendering

jsx
<AppScreen>
  {openAppId ? <Suspense fallback={<Splash />}>{<Component />}</Suspense> : <Home />}
</AppScreen>

Each app has its own <Component> and manages its internal routes/state.

7. Transitions

  • Open: app icon scales up, screen fades/slides in.
  • Close: reverse, with back-gesture or button.
  • Use Framer Motion or CSS transitions; respect prefers-reduced-motion.

8. Gestures + keyboard

  • Swipe up → back to home.
  • Swipe down from top → notifications panel.
  • ESC / browser back → back in stack.
  • Tab through apps in the grid.

Web doesn't have native gestures; intercept pointermove + threshold for swipe simulation.

9. Notifications

  • A separate layer above the screen stack.
  • Push-style notifications (toast / banner) animated in.
  • Each app can register a notification handler.
  • Tapping a notification opens the relevant app.

10. Status bar / dock

Fixed positioning; safe areas for notch (env(safe-area-inset-top) on iOS Safari).

11. Persistence

  • Selected app, lock state, app-specific state in localStorage or sessionStorage.
  • On reload, restore where the user was (with optional unlock requirement).

12. Bundle strategy

  • Initial bundle: framework + home + lock screen only.
  • Each app: separately lazy-loaded chunk.
  • Use <link rel="prefetch"> for the dock apps so they open instantly.

13. Accessibility

  • Each app icon is a <button> with label.
  • Status bar info read by screen readers via aria-label (don't speak time every second; only on focus).
  • Focus management when entering/exiting apps.
  • Reduced-motion: disable animations.

14. Real-world parallels

  • iOS / Android / Windows Phone home screens.
  • Web OSes (CrossOver, ChromeOS launcher).
  • Internal portal apps that group multiple tools (Salesforce App Cloud).

Interview framing

"Composition: status bar + screen stack + dock + notification layer. The screen stack is a tiny back-stack — open pushes, back pops. Apps live in a registry — each declares id, name, icon, and a lazy-loaded Component — so adding an app is one entry. Lock screen gates entry; home shows the grid; selecting an app renders its Component inside the AppScreen with Suspense. Gestures simulate swipe-up-to-home and swipe-down-for-notifications via pointer events with thresholds. Bundle: framework + home initially; each app lazy-loaded; prefetch the dock apps. Persistence in localStorage. Accessibility: focus management, labels, reduced-motion."

Follow-up questions

  • Why use a registry pattern for apps?
  • How do you simulate swipe gestures on web?
  • How do you handle the back button vs in-app navigation?
  • How do you keep the initial bundle small with many apps?

Common mistakes

  • Loading all apps upfront — huge bundle.
  • No back-stack — confused navigation.
  • Status bar info that animates every second (battery, time).
  • Modal layer that blocks the status bar.

Performance considerations

  • Lazy-load apps. Memoize home grid. Use transform-only for transitions. Prefetch likely-next app.

Edge cases

  • Refresh mid-app — restore?
  • Notification while in an app — toast vs banner vs interrupt.
  • Multi-window simulation.

Real-world examples

  • iOS/Android home screens; ChromeOS launcher; internal multi-tool portals.

Senior engineer discussion

Seniors structure around a registry, lazy-load aggressively, build a clean back-stack, and design accessibility + reduced-motion in from the start.

Related questions