Dark Mode System Low-Level Design

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



  
  
  Logo


/* 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.

{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How do you prevent the flash of wrong theme on page load?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Place a synchronous inline script in the HTML <head> that reads the theme preference from localStorage and sets data-theme on <html> before the browser renders any content. This script runs before CSS is applied, so there is no visible flash. The key constraint: it must be synchronous (not deferred or async) and must execute before any stylesheet or body content. On first visit (no localStorage key), read the OS preference via window.matchMedia("(prefers-color-scheme: dark)").matches to pick the correct default without a flash.”}},{“@type”:”Question”,”name”:”What are the three states a dark mode preference should support?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Light (explicit): always use light theme regardless of OS setting. Dark (explicit): always use dark theme regardless of OS setting. System (follow OS): use the OS dark/light preference and update live if the user changes their OS setting. "System" is the default and respects accessibility preferences. The system value requires listening to the prefers-color-scheme media query change event at runtime so the page updates without a reload when the user switches their OS from light to dark at dusk.”}},{“@type”:”Question”,”name”:”How do you sync dark mode preference across devices?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Save the preference to the server on change (PATCH /users/me/preferences with {theme: "dark"}). On login, fetch the preference and write it to localStorage. This way, a user who sets dark mode on their phone sees dark mode when they open a browser on their laptop (after login). Use optimistic UI: apply the change locally immediately, then sync to the server in the background. If the server update fails, roll back the localStorage value and show an error.”}},{“@type”:”Question”,”name”:”How do CSS custom properties enable theming?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Define color variables on :root for the light theme (the default). Override them on [data-theme="dark"] or .dark. Every component references the variable (var(–color-bg)) rather than a literal color (#ffffff). When you toggle the data-theme attribute on <html>, all variables update in a single repaint — no class changes needed on individual elements. This scales to hundreds of components: add a new component using existing variables and it automatically supports both themes without any theme-specific code.”}},{“@type”:”Question”,”name”:”How do you handle dark mode in server-side rendered pages?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”The server cannot read localStorage. Two options: (1) Cookie-based: write document.cookie = "theme=dark; path=/; SameSite=Lax" alongside localStorage on preference change. The server reads this cookie on each request and sets the data-theme attribute in the rendered HTML, matching what the inline script would set. This eliminates hydration mismatch in React/Next.js. (2) Accept the flash for unauthenticated users; for authenticated users, store the preference in the database and include it in the SSR context via the session.”}}]}

Dark mode and theme system design is discussed in Apple system design interview questions.

Dark mode and cross-platform preference sync design is covered in Google system design interview preparation.

Dark mode and UI theming system design is discussed in Meta system design interview guide.

Scroll to Top