DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

Designing Dark Mode for a Firefox Extension: Beyond Just Inverting Colors

Designing Dark Mode for a Firefox Extension: Beyond Just Inverting Colors

Every new tab extension should support dark mode. But good dark mode isn't just filter: invert() or flipping colors — it requires intentional design decisions. Here's how I implemented dark mode in the Weather & Clock Dashboard extension.

The Core Principle: Surfaces, Not Just Colors

Good dark mode thinks in surfaces:

  • Background: 0% lightness (true black) vs 12-15% (near-black)
  • Surface 1 (cards, panels): slightly lighter than background
  • Surface 2 (hover states): slightly lighter than Surface 1
  • Text primary: ~87% opacity white
  • Text secondary: ~60% opacity white

Material Design's dark theme guide is worth reading: this elevation-based approach prevents dark mode from looking flat.

CSS Custom Properties for Theming

:root {
  /* Light mode (default) */
  --bg-primary: #f0f2f5;
  --bg-surface: rgba(255, 255, 255, 0.85);
  --bg-surface-hover: rgba(255, 255, 255, 0.95);
  --text-primary: #1a1a2e;
  --text-secondary: #4a5568;
  --text-muted: #718096;
  --border: rgba(0, 0, 0, 0.08);
  --shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

[data-theme="dark"] {
  --bg-primary: #0f0f14;
  --bg-surface: rgba(255, 255, 255, 0.06);
  --bg-surface-hover: rgba(255, 255, 255, 0.1);
  --text-primary: rgba(255, 255, 255, 0.87);
  --text-secondary: rgba(255, 255, 255, 0.6);
  --text-muted: rgba(255, 255, 255, 0.38);
  --border: rgba(255, 255, 255, 0.08);
  --shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}

body {
  background: var(--bg-primary);
  color: var(--text-primary);
}
Enter fullscreen mode Exit fullscreen mode

Applying the Theme

const STORAGE_KEY = 'theme';

async function initTheme() {
  const { theme } = await browser.storage.local.get(STORAGE_KEY);
  applyTheme(theme || getSystemTheme());
}

function getSystemTheme() {
  return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}

function applyTheme(theme) {
  document.documentElement.setAttribute('data-theme', theme);
  // Update toggle button state
  const toggle = document.getElementById('theme-toggle');
  if (toggle) toggle.setAttribute('aria-pressed', theme === 'dark');
}

async function toggleTheme() {
  const current = document.documentElement.getAttribute('data-theme');
  const next = current === 'dark' ? 'light' : 'dark';
  applyTheme(next);
  await browser.storage.local.set({ [STORAGE_KEY]: next });
}

// Initialize on load
initTheme();

// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
  browser.storage.local.get(STORAGE_KEY).then(({ theme }) => {
    if (!theme) applyTheme(e.matches ? 'dark' : 'light');
  });
});
Enter fullscreen mode Exit fullscreen mode

The Toggle Button

<button 
  id="theme-toggle" 
  class="icon-btn theme-toggle"
  aria-label="Toggle dark mode"
  aria-pressed="false"
  title="Toggle dark/light mode (D)"
>
  <svg class="icon-sun" viewBox="0 0 24 24"><!-- sun icon --></svg>
  <svg class="icon-moon" viewBox="0 0 24 24"><!-- moon icon --></svg>
</button>
Enter fullscreen mode Exit fullscreen mode
.theme-toggle .icon-sun {
  display: block;
}
.theme-toggle .icon-moon {
  display: none;
}
[data-theme="dark"] .theme-toggle .icon-sun {
  display: none;
}
[data-theme="dark"] .theme-toggle .icon-moon {
  display: block;
}
Enter fullscreen mode Exit fullscreen mode

Transition Animation

A smooth transition makes the theme switch feel polished:

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

/* Don't transition on page load — only on user action */
body.no-transition {
  transition: none !important;
}
Enter fullscreen mode Exit fullscreen mode
async function toggleTheme() {
  const current = document.documentElement.getAttribute('data-theme');
  const next = current === 'dark' ? 'light' : 'dark';

  // Enable transition
  document.body.classList.remove('no-transition');

  applyTheme(next);
  await browser.storage.local.set({ [STORAGE_KEY]: next });
}

// Prevent flash of wrong theme
async function initTheme() {
  // Disable transitions during init
  document.body.classList.add('no-transition');

  const { theme } = await browser.storage.local.get(STORAGE_KEY);
  applyTheme(theme || getSystemTheme());

  // Re-enable transitions after a frame
  requestAnimationFrame(() => {
    document.body.classList.remove('no-transition');
  });
}
Enter fullscreen mode Exit fullscreen mode

Common Dark Mode Pitfalls

1. Images with white backgrounds

/* Invert images in dark mode if they have white backgrounds */
[data-theme="dark"] img.light-bg {
  filter: invert(0.85) hue-rotate(180deg);
}
Enter fullscreen mode Exit fullscreen mode

2. Transparent inputs

input, textarea {
  background: var(--bg-surface);
  color: var(--text-primary);
  border: 1px solid var(--border);
}

/* Important: explicitly set color, not just background */
input::placeholder {
  color: var(--text-muted);
}
Enter fullscreen mode Exit fullscreen mode

3. Third-party embeds (weather icons)

/* Soften bright weather icons in dark mode */
[data-theme="dark"] .weather-icon {
  opacity: 0.9;
  filter: brightness(0.9);
}
Enter fullscreen mode Exit fullscreen mode

Respecting the System Default

A good extension respects prefers-color-scheme when no preference has been saved:

/* Pure CSS approach — no JS needed for the initial state */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --bg-primary: #0f0f14;
    /* ... rest of dark theme variables */
  }
}
Enter fullscreen mode Exit fullscreen mode

This prevents the flash of light mode before JS loads.


The Weather & Clock Dashboard uses this exact approach — try toggling dark mode after installing.

css #webdev #firefox #browserextension #darkmode

Top comments (0)