# How Theme Switching and CSS Variables Work in pi-web

> Discover how pi-web utilizes CSS variables and a React hook for seamless theme switching between light and dark modes, automatically updating styles.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: deep-dive
- Published: 2026-07-23

---

**Pi‑web implements a light/dark theme system using a React hook that toggles a CSS class on the document root, while visual styles rely on CSS custom properties defined in [`globals.css`](https://github.com/agegr/pi-web/blob/main/globals.css) that automatically update when the theme changes.**

The pi-web repository handles theme switching through a coordinated system involving client-side storage, React state management, and CSS custom properties. This architecture ensures that theme preferences persist across sessions while delivering instant visual updates without JavaScript re-rendering overhead. The implementation spans four key files that handle initialization, state management, UI integration, and styling.

## Architecture Overview

The theme system operates on a simple but effective pattern: a **dark** class toggled on `document.documentElement` drives all visual changes through CSS variables. When this class is present, the browser automatically applies dark mode values; when absent, light mode defaults take effect. This approach leverages the browser's native CSS variable engine for zero-cost runtime theme updates.

## The useTheme Hook

The core logic resides in [`hooks/useTheme.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useTheme.ts), which exposes theme state and a toggle function to the React component tree.

The hook subscribes to theme changes using **`useSyncExternalStore`**, ensuring compatibility with React 18+ concurrent features and server-side rendering. It reads the current theme via a `getSnapshot` function that checks for the presence of the **dark** class on the document root.

```typescript
// hooks/useTheme.ts – core logic
export function useTheme() {
  const theme = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
  
  const toggleTheme = useCallback((origin) => {
    const next = getSnapshot() === "dark" ? "light" : "dark";
    
    // Apply class and persist
    if (next === "dark") {
      document.documentElement.classList.add("dark");
    } else {
      document.documentElement.classList.remove("dark");
    }
    
    localStorage.setItem("pi-theme", next);
    listeners.forEach(cb => cb());
  }, []);
  
  return { theme, toggleTheme, isDark: theme === "dark" };
}

```

The `toggleTheme` function optionally leverages the experimental **View‑Transition API** to animate the theme change smoothly. When this API is unavailable, it falls back to immediate class toggling. The new theme value is immediately persisted to `localStorage` under the key **`pi-theme`**.

## CSS Variable System

All visual theming relies on CSS custom properties declared in [`app/globals.css`](https://github.com/agegr/pi-web/blob/main/app/globals.css). The file defines parallel variable sets scoped to `:root` for light mode and `.dark` for dark mode.

```css
/* app/globals.css – variable definitions */
:root {
  --bg: #ffffff;
  --bg-panel: #f5f5f5;
  --bg-hover: #eeeeee;
  --bg-selected: #e8e8e8;
}

.dark {
  --bg: #1a1a1a;
  --bg-panel: #242424;
  --bg-hover: #2e2e2e;
  --bg-selected: #383838;
}

/* Component usage */
.main-panel {
  background: var(--bg);
}

```

Because components reference these variables via `var(--bg)` and similar declarations, theme changes require no JavaScript intervention in the render cycle. The browser recomputes styles instantly when the **dark** class toggles.

## Initialization and Persistence

To prevent flash of unstyled content or incorrect initial themes, [`app/layout.tsx`](https://github.com/agegr/pi-web/blob/main/app/layout.tsx) injects a blocking script that runs before React hydration.

This script checks `localStorage` for the **`pi-theme`** key and applies the **dark** class to `document.documentElement` if the saved value equals "dark". This ensures the correct theme renders immediately, eliminating any visual flicker during page load.

```tsx
// Inline script in app/layout.tsx (line 28)
<script dangerouslySetInnerHTML={{
  __html: `
    const theme = localStorage.getItem('pi-theme');
    if (theme === 'dark') {
      document.documentElement.classList.add('dark');
    }
  `
}} />

```

## UI Integration

The [`AppShell.tsx`](https://github.com/agegr/pi-web/blob/main/AppShell.tsx) component consumes the `useTheme` hook and wires it to the theme toggle button in the application's top navigation bar.

```tsx
// components/AppShell.tsx (lines 35-36)
const { isDark, toggleTheme } = useTheme();

<button onClick={toggleTheme}>
  {isDark ? 'Switch to Light' : 'Switch to Dark'}
</button>

```

This separation of concerns keeps the UI components agnostic of storage mechanisms and CSS implementation details, focusing solely on presentation and user interaction.

## Summary

- **State Management**: Theme state lives in `document.documentElement.classList` and `localStorage` under the key `pi-theme`.
- **React Hook**: `useTheme` in [`hooks/useTheme.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useTheme.ts) provides `isDark`, `theme`, and `toggleTheme` via `useSyncExternalStore`.
- **CSS Variables**: [`app/globals.css`](https://github.com/agegr/pi-web/blob/main/app/globals.css) defines light and dark variable sets under `:root` and `.dark` selectors.
- **Hydration Safety**: [`app/layout.tsx`](https://github.com/agegr/pi-web/blob/main/app/layout.tsx) includes an inline script to restore the theme before React hydrates.
- **Performance**: Visual updates occur through CSS variable recomputation rather than React re-renders.

## Frequently Asked Questions

### How does pi-web prevent theme flickering on page load?

A synchronous script in [`app/layout.tsx`](https://github.com/agegr/pi-web/blob/main/app/layout.tsx) checks `localStorage` for the `pi-theme` value and adds the **dark** class to the document root immediately during HTML parsing. This executes before React hydration, ensuring the correct CSS variables apply before the user sees the page.

### Why does the useTheme hook use useSyncExternalStore instead of useState?

The `useSyncExternalStore` hook treats the DOM class list as an external mutable source, allowing React to safely subscribe to changes that might occur outside its control. This pattern supports concurrent rendering features and prevents hydration mismatches between server and client environments.

### Where are the theme colors actually defined?

All color definitions reside in [`app/globals.css`](https://github.com/agegr/pi-web/blob/main/app/globals.css). Light mode values are assigned to the `:root` selector, while dark mode overrides are scoped under the `.dark` class. Components reference these variables using standard `var(--variable-name)` syntax throughout the stylesheet.

### Does pi-web support animated theme transitions?

Yes. The `toggleTheme` function in [`hooks/useTheme.ts`](https://github.com/agegr/pi-web/blob/main/hooks/useTheme.ts) attempts to use the experimental View‑Transition API to animate the theme change. If the browser does not support this API, it gracefully falls back to immediate class toggling without animation.