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