How Archify's Theme Toggle System Works with CSS Custom Properties and the data-theme Attribute

Archify implements a lightweight theme toggle using CSS custom properties and a data-theme attribute on the <html> element to switch between dark and light modes instantly without page reloads.

Archify is an open-source diagram visualization tool that ships with a self-contained, zero-dependency theme toggle implementation. The system leverages CSS custom properties (variables) and a data-theme attribute to provide cascade-free visual switching, with all logic embedded directly in examples/web-app.html.

CSS Variable Architecture

The theming engine relies on two scoped CSS variable blocks that define color values for every UI element.

Dark and Light Variable Blocks

In examples/web-app.html, the styles define variables under two selectors:

  • :root, [data-theme="dark"] – Contains the default dark-mode palette (e.g., --bg: #020617, --text: #ffffff, --toolbar-bg)
  • [data-theme="light"] – Overrides those variables with light-mode values (e.g., --bg: #f8fafc, --text: #0f172a)

All components consume these variables via var(--name). When JavaScript swaps the data-theme attribute on the <html> element, every dependent style updates automatically through CSS inheritance.

<style>
  :root,
  [data-theme="dark"] {
    --bg: #020617;
    --text: #ffffff;
    --toolbar-bg: #1e293b;
    /* additional dark variables */
  }

  [data-theme="light"] {
    --bg: #f8fafc;
    --text: #0f172a;
    --toolbar-bg: #e2e8f0;
    /* additional light variables */
  }
</style>

Source: examples/web-app.html (lines 40-84)

Theme Resolution Strategy

Before the first paint, an IIFE named Archify.theme resolves the initial theme through a three-tier priority system.

Priority Order: URL, localStorage, then System Preference

The resolveInitial() function checks sources in this strict sequence:

  1. URL query parameter (?theme=light or ?theme=dark) – Enables deterministic screenshots or forced themes via shareable links
  2. localStorage key archify-theme – Persists the user's last explicit choice across sessions
  3. prefers-color-scheme media query – Falls back to the OS-level setting when no explicit preference exists
function resolveInitial() {
  const fromUrl = urlOverride();
  if (fromUrl) return fromUrl;
  const saved = readStored();
  if (saved === 'light' || saved === 'dark') return saved;
  return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}

The resolved value is immediately applied to document.documentElement via html.setAttribute('data-theme', theme).

Source: examples/web-app.html (lines 13-44)

Toggle Implementation

The toolbar button (#btn-theme) exposes a toggle() function that flips the current state and persists the choice.

The apply() and toggle() Functions

The apply(theme) function writes the attribute, updates the button's icon (☾/☼) and label text, and sets aria-pressed for accessibility. The toggle() function reads the current data-theme value, computes the inverse, and triggers apply():

function toggle() {
  const next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
  apply(next);
  writeStored(next);
}

Persistence with localStorage

The writeStored(v) function wraps localStorage.setItem('archify-theme', v) in a try-catch block to prevent crashes in privacy mode or sandboxed environments. Similarly, readStored() safely retrieves the value, returning null if storage is inaccessible.

Source: examples/web-app.html (lines 45-56)

OS-Level Theme Synchronization

When the user has not set an explicit preference (no URL parameter and no stored value), the system listens for live OS theme changes.

Listening to prefers-color-scheme

The script attaches a change listener to window.matchMedia('(prefers-color-scheme: light)'). If the media query matches, the system applies the light theme; otherwise, it applies dark. This listener only executes when urlOverride() and readStored() both return null, ensuring user overrides take precedence.

const media = window.matchMedia('(prefers-color-scheme: light)');
media.addEventListener('change', e => {
  if (urlOverride() || readStored()) return;
  apply(e.matches ? 'light' : 'dark');
});

Source: examples/web-app.html (lines 60-70)

SVG Export Compatibility

Exported diagrams must retain theme fidelity even when viewed outside the Archify application.

Self-Theming Exported Assets

During SVG export, the script probes the computed styles for both themes using a temporary DOM element. It extracts the full set of custom properties, then injects two CSS blocks into the exported SVG. This allows the exported file to respond to prefers-color-scheme on external hosts, ensuring colors remain correct in both dark and light viewing contexts.

The resolveVars logic captures current variable values and writes them into <style> tags within the SVG, making the asset self-contained and theme-aware.

Source: examples/web-app.html (lines 68-84)

Summary

  • CSS Custom Properties – All UI colors reference variables defined under :root (dark) and [data-theme="light"] selectors, enabling instant updates via attribute changes.
  • Triple-Tier Resolution – The resolveInitial() function checks URL parameters first, then localStorage, then system preferences.
  • State Persistence – The toggle() function writes choices to localStorage under the key archify-theme.
  • OS Sync – A matchMedia listener reacts to prefers-color-scheme changes when no explicit user preference exists.
  • Export Safety – The resolveVars mechanism embeds both theme definitions into exported SVGs for standalone theme switching.

Frequently Asked Questions

How does the data-theme attribute trigger the theme change?

The data-theme attribute acts as a CSS selector hook. When JavaScript sets data-theme="light" on the <html> element, the [data-theme="light"] CSS block becomes active, overriding the :root variables. Because all UI elements reference these variables, the visual update happens instantly without JavaScript manipulating individual styles.

What happens if localStorage is disabled or private browsing mode is active?

The readStored() and writeStored() functions wrap all localStorage calls in try-catch blocks. If storage is inaccessible, the system silently falls back to URL parameters or system preferences, ensuring the theme toggle still functions without throwing errors.

Can I force a specific theme via URL for sharing diagrams?

Yes. Appending ?theme=dark or ?theme=light to the URL overrides both localStorage and system preferences. The urlOverride() function checks URLSearchParams early in the resolution chain, making it useful for generating screenshots or sharing links with a locked visual style.

How does the exported SVG know which theme to display?

The export process captures the computed values of all CSS custom properties for both themes and embeds them as <style> blocks inside the SVG. The exported file includes prefers-color-scheme media queries, allowing it to self-theme based on the viewer's OS settings when displayed in browsers or image viewers that support CSS.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →