# How to Implement Dark Mode Using Astryx Theming: A Complete Guide

> Learn to implement dark mode with Astryx theming. This guide covers the Theme provider and useTheme hook to control your app's color mode and tokens.

- Repository: [Meta/astryx](https://github.com/facebook/astryx)
- Tags: how-to-guide
- Published: 2026-08-05

---

**Astryx provides a `Theme` provider component and `useTheme` hook that expose the current color mode (`light` | `dark` | `system`) and automatically resolve token values for your chosen theme.**

The Astryx theming system is designed for flexibility and performance, allowing developers to toggle between light and dark modes with minimal boilerplate. According to the `facebook/astryx` source code, the architecture centers on a context-based provider that synchronizes HTML attributes, injects runtime CSS, and exposes resolved design tokens to any component in the tree.

## Core Architecture of Astryx Theming

Understanding how Astryx implements dark mode requires familiarity with five key pieces:

- **`Theme` component** — Wraps part of the UI, registers a theme, injects generated CSS, and (for the root provider) syncs `<html data-theme>` and `<html data-astryx-theme>` attributes. Located in [`packages/core/src/theme/Theme.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/Theme.tsx).

- **`useTheme` hook** — Reads the nearest `ThemeContext` and returns the resolved token map and effective mode. Falls back to root HTML attributes when no provider is present. Found in [`packages/core/src/theme/useTheme.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/useTheme.ts).

- **`ThemeContext`** — Holds the theme object and requested mode (`system`, `light`, `dark`). Defined in [`packages/core/src/theme/useTheme.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/useTheme.ts).

- **`defineTheme`** — Creates a `DefinedTheme` with token overrides and optional component style overrides. Lives in [`packages/core/src/theme/defineTheme.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/defineTheme.ts).

- **`useRootThemeSync`** — Internal hook inside [`Theme.tsx`](https://github.com/facebook/astryx/blob/main/Theme.tsx) that writes `data-theme` and `data-astryx-theme` to `<html>`, setting `color-scheme` CSS for native UI elements.

When `Theme` renders with `mode="dark"`, the provider executes four steps: registers the theme, injects CSS, calls `useRootThemeSync` to update `<html data-theme="dark">`, and passes the mode through context.

## Defining a Theme with Light/Dark Token Pairs

Astryx themes use arrays to specify values for each mode. The `resolveThemeTokens` function in [`packages/core/src/theme/tokens.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/tokens.ts) selects the appropriate entry based on the effective mode.

```tsx
import {defineTheme} from '@astryxdesign/core/theme';

export const ocean = defineTheme({
  name: 'ocean',
  tokens: {
    '--color-accent': ['#0064E0', '#48CAE4'],          // [light, dark]
    '--color-background': ['#FFFFFF', '#0A0A0A'],
    '--color-text-primary': ['#1F1F1F', '#E5E5E5'],
    '--color-border': ['#E0E0E0', '#333333'],
  },
});

```

Token values are always ordered `[light, dark]`. This convention enables automatic resolution without conditional logic in your components.

## Implementing Dark Mode: Three Patterns

### Static Dark Mode

Force dark appearance regardless of OS settings by setting `mode="dark"` on the root provider:

```tsx
import {Theme} from '@astryxdesign/core/theme';
import {App} from './App';
import {ocean} from './themes/ocean';

export default function Root() {
  return (
    <Theme theme={ocean} mode="dark">
      <App />
    </Theme>
  );
}

```

The `useRootThemeSync` logic in [`packages/core/src/theme/Theme.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/Theme.tsx) writes `data-theme="dark"` to `<html>`, making `color-scheme: dark` apply globally.

### System-Aware Dark Mode

Respect the user's OS preference by omitting `mode` or setting `mode="system"`:

```tsx
<Theme theme={ocean} mode="system">
  <App />
</Theme>

```

When `mode="system"`, `useTheme` evaluates `(prefers-color-scheme: dark)` via internal media query logic. The effective mode updates automatically when the OS setting changes.

**Server-side rendering note:** To prevent a flash of incorrect colors, set `data-theme` on your HTML root element:

```html
<html lang="en" data-theme="dark">

```

See the SSR comment in [`Theme.tsx`](https://github.com/facebook/astryx/blob/main/Theme.tsx) for implementation details.

### User-Controlled Dark Mode Toggle

Store mode in React state and pass it to the provider for full user control:

```tsx
import {useState} from 'react';
import {Theme} from '@astryxdesign/core/theme';
import {ocean} from './themes/ocean';
import {Button} from '@astryxdesign/core/Button';
import {useTheme} from '@astryxdesign/core/theme';

export function DarkModeDemo() {
  const [mode, setMode] = useState<'light' | 'dark' | 'system'>('system');

  const toggle = () =>
    setMode(prev => 
      prev === 'light' ? 'dark' : prev === 'dark' ? 'system' : 'light'
    );

  const {mode: effective, token} = useTheme();

  return (
    <Theme theme={ocean} mode={mode}>
      <div style={{padding: 24, background: token('--color-background')}}>
        <p>Current effective mode: <strong>{effective}</strong></p>
        <Button 
          label={`Mode: ${mode}`} 
          onClick={toggle} 
        />
      </div>
    </Theme>
  );
}

```

The update flow works as follows:

1. `setMode` updates the `mode` prop
2. `Theme` calls `useRootThemeSync` to update `<html data-theme>`
3. `useTheme` recomputes `effectiveMode` (resolving system preference if needed)
4. `token('--color-background')` returns the correct hex for the active mode

## Accessing Tokens Outside of CSS

For canvas rendering, charts, or other non-CSS contexts, use the `token` helper from `useTheme`:

```tsx
import {useTheme} from '@astryxdesign/core/theme';

export function CanvasChart() {
  const {token, mode} = useTheme();
  const ctx = canvasRef.current?.getContext('2d');
  
  ctx.fillStyle = token('--color-accent');
  ctx.fillRect(0, 0, width, height);
  
  // mode indicates 'light' | 'dark' for conditional logic
}

```

This pattern appears in Astryx's charting package at [`packages/charts/src/useChartColors.ts`](https://github.com/facebook/astryx/blob/main/packages/charts/src/useChartColors.ts).

## Key Implementation Files

| File | Purpose |
|------|---------|
| [`packages/core/src/theme/Theme.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/Theme.tsx) | Provider component with root attribute sync and CSS injection |
| [`packages/core/src/theme/useTheme.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/useTheme.ts) | Hook for token resolution and effective mode detection |
| [`packages/core/src/theme/tokens.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/tokens.ts) | Token definitions and `resolveThemeTokens` function |
| [`packages/core/src/theme/defineTheme.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/defineTheme.ts) | Theme creation utility |
| [`packages/cli/assets/templates/blocks/components/Theme/ThemeSwitcher.tsx`](https://github.com/facebook/astryx/blob/main/packages/cli/assets/templates/blocks/components/Theme/ThemeSwitcher.tsx) | Reference implementation for theme switching UI |
| [`apps/storybook/stories/Theme.stories.tsx`](https://github.com/facebook/astryx/blob/main/apps/storybook/stories/Theme.stories.tsx) | Interactive dark mode demonstrations |

## Summary

- **Define themes** with `defineTheme`, using `[light, dark]` arrays for token values
- **Wrap applications** with `Theme` provider, passing `mode` as `"light"`, `"dark"`, or `"system"`
- **Read resolved values** via `useTheme()` hook, which provides both tokens and effective mode
- **Sync to root HTML** happens automatically via `useRootThemeSync`, enabling `color-scheme` for native UI
- **Support SSR** by setting `data-theme` on the server-rendered `<html>` element

## Frequently Asked Questions

### How does Astryx detect the system color preference?

When `mode="system"`, the `useTheme` hook evaluates `window.matchMedia('(prefers-color-scheme: dark)')` internally. If no `Theme` provider exists in the component tree, the hook falls back to reading the `<html data-theme>` attribute, then finally checks the OS preference directly.

### Can I nest multiple Theme providers?

Yes. Nested `Theme` providers create scoped themes for subtrees. Only the root provider (detected via internal logic in [`Theme.tsx`](https://github.com/facebook/astryx/blob/main/Theme.tsx)) calls `useRootThemeSync` to modify `<html>` attributes. Child providers affect their descendants without changing global settings. This enables admin dashboards with per-section theming.

### What happens if I don't use a Theme provider?

Components calling `useTheme()` degrade gracefully. The hook falls back to the root `<html data-theme>` attribute, then to `prefers-color-scheme` media query. Without either, tokens return their light-mode defaults. For predictable results, always include a root provider.

### Are Astryx themes compatible with CSS custom properties?

Yes. The `token()` function returns resolved string values, but Astryx also injects CSS custom properties via `wrapperStyles` in the `Theme` component. These properties follow naming conventions like `--color-background` and update instantly when mode changes, enabling pure-CSS theming for stylesheets outside the React tree.