# Dark Mode System Low-Level Design

Source: https://www.techinterview.org/post/3233467854/lld-dark-mode/
Updated: 2026-04-17 · techinterview.org

## Dark Mode System — Low-Level Design

A dark mode system persists a user's theme preference, propagates it across devices, and resolves conflicts between user preference and system OS setting. This appears in interviews at Apple, Google, and Meta where cross-platform UX consistency is a core concern.

## Core Data Model


```
UserPreference
  user_id           BIGINT PK
  theme             TEXT NOT NULL DEFAULT 'system'  -- 'light', 'dark', 'system'
  updated_at        TIMESTAMPTZ NOT NULL

-- 'system' means: follow the OS-level dark/light preference
-- 'light' / 'dark' means: override OS preference explicitly
```


## Theme Resolution Priority


```
function resolveTheme(userPreference, osPreference) {
  // Priority: explicit user choice > OS setting > fallback to light
  if (userPreference === 'light' || userPreference === 'dark') {
    return userPreference;
  }
  // userPreference === 'system'
  if (osPreference === 'dark') return 'dark';
  return 'light';
}

// On the web: OS preference via CSS media query
// prefers-color-scheme: dark
const osPreference = window.matchMedia('(prefers-color-scheme: dark)').matches
  ? 'dark' : 'light';

// Listen for OS changes
window.matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', e => {
    if (currentUserPreference === 'system') {
      applyTheme(e.matches ? 'dark' : 'light');
    }
  });
```


## Avoiding Flash of Incorrect Theme (FOIT)

The biggest dark mode UX problem: page loads briefly in light mode before JS runs and applies dark mode, causing a visible flash.


```
<!-- Inline script in  — executes before any rendering -->

  (function() {
    var pref = localStorage.getItem('theme') || 'system';
    var osIsDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    var theme = (pref === 'system') ? (osIsDark ? 'dark' : 'light') : pref;
    document.documentElement.setAttribute('data-theme', theme);
  })();


/* CSS: theme applied via data attribute on  */
[data-theme='dark'] {
  --bg-color: #1a1a1a;
  --text-color: #e8e8e8;
  --border-color: #333;
}
[data-theme='light'] {
  --bg-color: #ffffff;
  --text-color: #1a1a1a;
  --border-color: #e0e0e0;
}
```


The inline script runs synchronously before the browser paints. It reads localStorage (no network call) and sets data-theme on  before any CSS is applied.

## Syncing Preference Across Devices


```
-- Save to server on change
PATCH /users/me/preferences
{ "theme": "dark" }

-- Server:
UPDATE UserPreference
SET theme=%(theme)s, updated_at=NOW()
WHERE user_id=%(user_id)s;

-- On login: load preference and set localStorage
GET /users/me/preferences
→ { "theme": "dark", "updated_at": "2024-01-15T10:30:00Z" }

localStorage.setItem('theme', preference.theme);
applyTheme(resolveTheme(preference.theme, osPreference));
```


## CSS Architecture for Theming


```
/* Option 1: CSS custom properties (recommended) */
:root {
  --color-bg: white;
  --color-text: black;
  --color-primary: #0070f3;
}
[data-theme='dark'] {
  --color-bg: #111;
  --color-text: #eee;
  --color-primary: #3b82f6;
}

/* Option 2: Tailwind dark mode */
/* tailwind.config.js: darkMode: 'class' */
/* Apply: className="bg-white dark:bg-gray-900 text-black dark:text-white" */

/* Option 3: CSS-in-JS (styled-components, Emotion) */
const theme = {
  light: { bg: 'white', text: 'black' },
  dark: { bg: '#111', text: '#eee' }
};
```


## Image and Media Handling


```
/* For SVG icons: use currentColor to inherit text color */
.icon { fill: currentColor; }

/* For photos: slight brightness reduction in dark mode */
[data-theme='dark'] img:not([data-no-invert]) {
  filter: brightness(0.9);
}
```


## Server-Side Rendering Considerations


```
// On SSR (Next.js, etc.): read preference from cookie
// Cookie set on the client alongside localStorage
document.cookie = `theme=${theme}; path=/; max-age=31536000; SameSite=Lax`;

// Server reads cookie to render the correct theme class server-side
// This avoids hydration mismatch between server HTML and client JS
export async function getServerSideProps({ req }) {
  const theme = req.cookies.theme || 'system';
  return { props: { initialTheme: theme } };
}
```


## Key Interview Points

- **Three states, not two:** Always model light, dark, and system. "System" follows OS preference and must update live when the user changes their OS setting at runtime.

- **Prevent flash via inline script:** The only reliable way to prevent flash on page load is a synchronous inline script in <head> that reads localStorage before first paint.

- **CSS custom properties > class toggling:** Variables propagate automatically to all components without class changes on every element. One attribute on <html> drives the whole theme.

- **Cookie for SSR:** localStorage is not available server-side. Set a cookie alongside localStorage so the server can render the correct theme and avoid hydration mismatch.

Dark mode and theme system design is discussed in [Apple system design interview questions](https://www.techinterview.org/post/3233460277/apple-interview-guide-2026-ios-systems-hardware-software-integration-and-icloud-architecture/).

Dark mode and cross-platform preference sync design is covered in [Google system design interview preparation](https://www.techinterview.org/post/3233460274/netflix-interview-guide-2026-streaming-architecture-recommendation-systems-and-engineering-excellence/).

Dark mode and UI theming system design is discussed in [Meta system design interview guide](https://www.techinterview.org/post/3233460272/meta-interview-guide-2026-facebook-instagram-whatsapp-engineering/).
