Implementing Dark Mode with System Theme Detection

Table of Contents
Big thanks to our contributors those make our blogs possible.

Our growing community of contributors bring their unique insights from around the world to power our blog. 

Introduction

Dark mode has quickly become more than just a design trend—it’s an accessibility feature that reduces eye strain in low-light environments, conserves battery life on OLED screens, and aligns with user preferences for aesthetics. Modern operating systems (Windows, macOS, iOS, Android) allow users to choose between light and dark themes at the system level. By detecting these preferences in your web or mobile app, you can automatically match the user’s theme, delivering a seamless experience without requiring manual toggles. In this comprehensive guide, we’ll explore how to implement dark mode with system theme detection across web and React Native platforms, cover best practices for theming, and share expert tips for a polished user experience.

Why System Theme Detection Matters

  1. User Comfort & Accessibility
    • Respecting system settings reduces visual fatigue for users who prefer dark environments.
    • Improves readability for those with light sensitivity or other visual impairments.
  2. Seamless UX
    • Eliminates the need for users to hunt for a manual dark–light toggle.
    • Ensures consistency with the user’s broader device experience.
  3. Energy Efficiency
    • On OLED and AMOLED displays, dark pixels consume less power—especially significant for battery-powered devices.
  4. Modern Expectations
    • System theme support is now standard in major platforms; users expect apps to honor their choice automatically.

Part 1: Implementing Dark Mode on the Web

1.1 Using the prefers-color-scheme Media Query

Modern browsers expose the user’s preferred theme via the CSS media query prefers-color-scheme. You can leverage this directly in CSS:

cssCopyEdit/* Base (light) theme */
:root {
  --background-color: #ffffff;
  --text-color: #222222;
}

/* Dark theme overrides */
@media (prefers-color-scheme: dark) {
  :root {
    --background-color: #1e1e1e;
    --text-color: #f5f5f5;
  }
}

/* Applying variables */
body {
  background-color: var(--background-color);
  color: var(--text-color);
}

How it works:

  • The browser evaluates the media query and applies the dark overrides if the user’s OS is set to dark mode.
  • No JavaScript is required—this is a purely CSS-based solution.

1.2 Dynamically Reacting to Changes

If you need to respond in JavaScript—perhaps to update charts or reinitialize components—you can listen for changes:

jsCopyEditconst darkQuery = window.matchMedia('(prefers-color-scheme: dark)');

function applyTheme(e) {
  if (e.matches) {
    document.documentElement.setAttribute('data-theme', 'dark');
  } else {
    document.documentElement.removeAttribute('data-theme');
  }
}

// Initial check
applyTheme(darkQuery);

// Listen for changes
darkQuery.addEventListener('change', applyTheme);

Couple this with CSS:

cssCopyEdithtml[data-theme="dark"] {
  --background-color: #1e1e1e;
  --text-color: #f5f5f5;
}

1.3 Providing a Manual Toggle

While auto-detection is ideal, some users appreciate control. Combine system detection with a toggle stored in localStorage:

jsCopyEdit// On page load
const userPref = localStorage.getItem('theme');
if (userPref) {
  document.documentElement.setAttribute('data-theme', userPref);
} else {
  // Fallback to system
  applyTheme(darkQuery);
}

// Toggle handler
document.getElementById('theme-toggle').addEventListener('click', () => {
  const current = document.documentElement.getAttribute('data-theme');
  const next = current === 'dark' ? 'light' : 'dark';
  document.documentElement.setAttribute('data-theme', next);
  localStorage.setItem('theme', next);
});

Best Practice: Respect the user’s explicit choice over the system setting by prioritizing the saved preference.

Part 2: Implementing Dark Mode in React Native

2.1 Using the Appearance API

React Native’s Appearance module provides access to the system color scheme:

jsCopyEditimport { useColorScheme } from 'react-native';

export default function App() {
  const scheme = useColorScheme(); // 'light' or 'dark'

  const backgroundStyle = {
    backgroundColor: scheme === 'dark' ? '#1e1e1e' : '#ffffff',
  };

  return (
    <View style={[styles.container, backgroundStyle]}>
      <Text style={{ color: scheme === 'dark' ? '#f5f5f5' : '#222222' }}>
        Hello, {scheme} mode!
      </Text>
    </View>
  );
}

This hook re-renders your component when the system theme changes, providing real-time updates.

2.2 Theming with Context

For larger apps, centralize your theme values using React Context:

jsCopyEdit// theme.js
import { createContext, useContext } from 'react';
import { useColorScheme } from 'react-native';

const lightTheme = {
  background: '#ffffff',
  text: '#222222',
  primary: '#0066cc',
};

const darkTheme = {
  background: '#1e1e1e',
  text: '#f5f5f5',
  primary: '#3399ff',
};

const ThemeContext = createContext(lightTheme);

export function ThemeProvider({ children }) {
  const scheme = useColorScheme();
  const theme = scheme === 'dark' ? darkTheme : lightTheme;
  return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;
}

export function useTheme() {
  return useContext(ThemeContext);
}

Wrap your app:

jsCopyEdit// App.js
import { ThemeProvider } from './theme';

export default function App() {
  return (
    <ThemeProvider>
      <MainRouter />
    </ThemeProvider>
  );
}

Consume theme:

jsCopyEditimport { useTheme } from './theme';

function Header() {
  const { background, text } = useTheme();
  return <View style={{ backgroundColor: background }}><Text style={{ color: text }}>Title</Text></View>;
}

Part 3: Best Practices and Expert Tips

3.1 Define a Comprehensive Color Palette

  • Semantic tokens: Use names like --color-background, --color-text-primary, --color-accent rather than “white” or “gray.”
  • Accessibility: Ensure sufficient contrast (WCAG 2.1 AA)—test your dark theme with tools like Contrast Checker.

3.2 Animate Theme Transitions

Smooth fades reduce jarring shifts:

Web (CSS):

cssCopyEdithtml {
  transition: background-color 0.3s ease, color 0.3s ease;
}

React Native (Animated API):

jsCopyEditimport { useEffect, useRef } from 'react';
import { Animated, useColorScheme } from 'react-native';

function ThemedView({ children }) {
  const scheme = useColorScheme();
  const animation = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    Animated.timing(animation, {
      toValue: scheme === 'dark' ? 1 : 0,
      duration: 300,
      useNativeDriver: false,
    }).start();
  }, [scheme]);

  const backgroundColor = animation.interpolate({
    inputRange: [0, 1],
    outputRange: ['#ffffff', '#1e1e1e'],
  });

  return <Animated.View style={{ backgroundColor }}>{children}</Animated.View>;
}

3.3 Test Across Devices and Browsers

  • Desktop & mobile browsers: Chrome, Safari, Firefox, Edge—each may have subtle differences.
  • Mobile simulators/emulators: iOS Simulator, Android Emulator, plus real-device testing for theme-change notifications.

3.4 Fallbacks and Graceful Degradation

  • Older browsers lacking prefers-color-scheme will default to your light theme.
  • Provide manual toggles prominently in settings or the UI header.

Conclusion

Implementing dark mode with system theme detection enhances user comfort, aligns with modern UX expectations, and demonstrates attention to detail. On the web, the prefers-color-scheme media query and JavaScript listeners provide robust, low-effort solutions. In React Native, the Appearance API and context-based theming ensure your mobile app remains in sync with OS preferences. By defining semantic color tokens, ensuring accessibility, animating transitions, and providing manual overrides, you’ll deliver a polished, inclusive experience across platforms. Start by auditing your current styles, integrate system detection, and watch user satisfaction—and nightly usage times—rise.

Let's connect on TikTok

Join our newsletter to stay updated

Sydney Based Software Solutions Professional who is crafting exceptional systems and applications to solve a diverse range of problems for the past 10 years.

Share the Post

Related Posts