# Light / dark switching

Material 3 ships both light and dark variants — toggle by setting data-theme on <html>.


Material 3's theme file ships both light and dark variants. Switch between them at runtime by toggling `data-theme="dark"` on the root `<html>` element. CSS specificity handles the rest.

> Excel is light-only by design. There's no `[data-theme="dark"]` block in `excel.css`. If you need dark mode, use Material 3 or build your own theme file with both variants. See [Custom themes](/docs/theming/custom-themes).

## React state-driven

Hold the theme mode in React state, sync to the DOM in an effect:

```tsx
import { useEffect, useState } from "react";

type ThemeMode = "light" | "dark";

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [mode, setMode] = useState<ThemeMode>("light");

  useEffect(() => {
    if (mode === "dark") {
      document.documentElement.dataset.theme = "dark";
    } else {
      delete document.documentElement.dataset.theme;
    }
  }, [mode]);

  return (
    <>
      <button onClick={() => setMode(mode === "light" ? "dark" : "light")}>
        Switch to {mode === "light" ? "dark" : "light"}
      </button>
      {children}
    </>
  );
}
```

Wrap your app with this provider. Anywhere a `<Pretable>` renders inside, the grid responds to the mode change automatically. The theme file's `[data-theme="dark"]` block declares the dark color overrides; CSS cascade does the rest.

## OS-respect (`prefers-color-scheme`)

For apps that should follow the OS dark-mode setting without asking:

```tsx
import { useEffect, useState } from "react";

function getSystemMode(): "light" | "dark" {
  if (typeof window === "undefined") return "light";
  return window.matchMedia("(prefers-color-scheme: dark)").matches
    ? "dark"
    : "light";
}

export function SystemThemeProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const [mode, setMode] = useState<"light" | "dark">(getSystemMode);

  useEffect(() => {
    const mql = window.matchMedia("(prefers-color-scheme: dark)");
    const handler = (e: MediaQueryListEvent) => {
      setMode(e.matches ? "dark" : "light");
    };
    mql.addEventListener("change", handler);
    return () => mql.removeEventListener("change", handler);
  }, []);

  useEffect(() => {
    if (mode === "dark") {
      document.documentElement.dataset.theme = "dark";
    } else {
      delete document.documentElement.dataset.theme;
    }
  }, [mode]);

  return <>{children}</>;
}
```

## Composition with density

`data-theme="dark"` and `data-density` are independent attributes. They compose:

```html
<html data-theme="dark" data-density="spacious">
  ...
</html>
```

Material's `[data-theme="dark"]` block overrides color tokens (cell background, text, accent, gridlines) without touching density tokens. Material's `[data-density="spacious"]` block overrides density tokens without touching colors. Both apply.

The engine's `useResolvedHeights` hook (in `@pretable/react`) listens for either attribute change via `MutationObserver` and re-renders the grid with new heights when density flips. No additional wiring needed.

## SSR considerations

If your app renders on the server, the initial HTML doesn't know the user's mode. Two patterns:

1. **Cookie-driven SSR.** Read a `theme` cookie server-side, set `<html data-theme="dark">` in the initial markup if it's `"dark"`. Avoids a flash of light content.
2. **Client-only.** Don't set `data-theme` server-side; let the React effect set it after hydration. Brief flash of light mode is acceptable for low-traffic apps.

The OS-respect pattern above is client-only by default — `getSystemMode` returns `"light"` server-side because `window` is undefined.

## Where to go next

- [Density switching](/docs/theming/density) — runtime compact/standard/spacious.
- [Override tokens](/docs/theming/override-tokens) — change colors per-mode by overriding inside `[data-theme="dark"]`.
- [Custom themes](/docs/theming/custom-themes) — author your own dark-mode block.
