News / Article

Dark Mode Implementation

Learn how we implemented smooth theme switching with localStorage persistence and system preference detection in this Astro boilerplate.

A
Astro Boilerplate
Dark Mode Implementation

Dark Mode Implementation

Dark mode isn’t just a trend—it’s expected by users. Here’s how this boilerplate handles theme switching elegantly.

The Challenge

Implementing dark mode seems simple until you consider:

  1. Flash of incorrect theme on page load
  2. System preference detection
  3. User override persistence
  4. Smooth transitions

Our solution addresses all four.

The Architecture

1. Early Theme Script

We inject a blocking script in the <head> to prevent flash:

<script is:inline>
  const theme =
    localStorage.getItem('theme') ||
    (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
  document.documentElement.dataset.theme = theme;
</script>

The is:inline directive ensures this runs before any rendering.

2. CSS Custom Properties

Theme changes swap entire color palettes:

:root,
[data-theme='light'] {
  --color-background: #f9f8f4;
  --color-foreground: #2d3a31;
}

[data-theme='dark'] {
  --color-background: #1a1f1c;
  --color-foreground: #e8e6e1;
}

3. Theme Toggle Component

A React component handles toggling:

export function ThemeToggle() {
  const [theme, setTheme] = useState('light');

  const toggle = () => {
    const next = theme === 'light' ? 'dark' : 'light';
    document.documentElement.dataset.theme = next;
    localStorage.setItem('theme', next);
    setTheme(next);
  };

  return <button onClick={toggle}>Toggle</button>;
}

4. Smooth Transitions

Colors transition smoothly:

:root {
  transition:
    background-color 300ms ease,
    color 300ms ease;
}

System Preference Detection

We respect user system preferences while allowing overrides:

  1. Check localStorage for saved preference
  2. Fall back to prefers-color-scheme media query
  3. Default to light mode if neither exists

Performance Considerations

  • No flash: Inline script blocks rendering until theme is set
  • No layout shift: Theme doesn’t affect layout
  • Minimal JS: Only ~200 bytes of blocking script

Accessibility

  • Theme state is visible in the toggle button
  • Color contrast meets WCAG AA in both modes
  • Reduced motion preferences are respected

Take This Further

You could extend this with:

  • Multiple theme options beyond light/dark
  • Theme per-page settings
  • Automatic theme switching based on time of day

The foundation is solid. Build on it!

Share this article