# How to Implement Dark Mode and Light Mode Switching with Astryx Theme Tokens

> Easily implement dark and light mode switching using Astryx theme tokens. Define tokens as light/dark tuples and let Astryx handle CSS compilation and theme synchronization for a seamless user experience.

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

---

**Astryx enables automatic dark and light mode switching by defining tokens as `[light, dark]` tuples that compile to CSS `light-dark()` functions, while the `<Theme>` provider component synchronizes the color scheme to the document root.**

Dark mode implementation in the facebook/astryx repository relies on a two-level architecture that separates token definition from runtime synchronization. By leveraging Astryx theme tokens defined as mode-aware tuples, applications can support automatic color scheme switching without JavaScript-based style injection. This approach utilizes native CSS `light-dark()` functions and document-level attributes to ensure zero-flicker theme transitions across your entire component tree.

## Understanding Astryx Theme Token Architecture

### Token Definition in [`defineTheme.ts`](https://github.com/facebook/astryx/blob/main/defineTheme.ts)

The foundation of mode switching resides in [`packages/core/src/theme/defineTheme.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/defineTheme.ts), where the `defineTheme` function processes token values. When a token is defined as a tuple containing two values—`[lightValue, darkValue]`—the framework automatically transforms it into a browser-native CSS `light-dark()` function.

### The `resolveTokenValue` Helper

Inside [`defineTheme.ts`](https://github.com/facebook/astryx/blob/main/defineTheme.ts), the `resolveTokenValue` helper function detects array-based tokens and wraps them accordingly:

```ts
// packages/core/src/theme/defineTheme.ts
function resolveTokenValue(value: TokenValue): string {
  if (Array.isArray(value)) {
    return `light-dark(${value[0]}, ${value[1]})`;
  }
  return value;
}

```

This conversion ensures that a single CSS custom property adapts to the user's preferred color scheme without runtime JavaScript overhead.

## Implementing the Theme Provider

### The `<Theme>` Component and Mode Prop

The [`packages/core/src/theme/Theme.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/Theme.tsx) file exports the `<Theme>` provider component, which accepts a `mode` prop of type `'light' | 'dark' | 'system'`. This component injects the theme's CSS variables and manages the document's color scheme through the `wrapperStyles` stylex definitions:

```tsx
// packages/core/src/theme/Theme.tsx
const wrapperStyles = stylex.create({
  base:   { display: 'contents', color: colorVars['--color-text-primary'], fontFamily: typographyVars['--font-family-body'] },
  light:  { colorScheme: 'light' },
  dark:   { colorScheme: 'dark' },
  system: { colorScheme: 'light dark' },
});

```

### Root Synchronization with `useRootThemeSync`

To ensure portals and server-rendered content respect the current mode, [`Theme.tsx`](https://github.com/facebook/astryx/blob/main/Theme.tsx) implements the `useRootThemeSync` hook. This hook updates the `<html>` element's attributes to drive the `light-dark()` resolution:

```tsx
// packages/core/src/theme/Theme.tsx
function useRootThemeSync(isNested, mode, themeName) {
  useIsomorphicLayoutEffect(() => {
    if (isNested) return;
    if (mode === 'light' || mode === 'dark') {
      document.documentElement.setAttribute('data-theme', mode);
    } else {
      document.documentElement.removeAttribute('data-theme');
    }
    document.documentElement.setAttribute(dataAttr('theme'), themeName);
  }, [isNested, mode, themeName]);
}

```

The `data-astryx-theme` attribute ensures scoped CSS reaches out-of-tree elements like React portals.

## Step-by-Step Implementation Guide

Follow these steps to implement dark mode and light mode switching with Astryx theme tokens in your application.

### Step 1: Define Mode-Aware Tokens

Create a theme using `defineTheme` from `@astryxdesign/core/theme/defineTheme`. Specify tokens that vary by mode as `[light, dark]` tuples, while keeping static values as strings:

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

export const oceanTheme = defineTheme({
  name: 'ocean',
  tokens: {
    '--color-accent': ['#0077B6', '#48CAE4'],       // [light, dark] tuple
    '--color-background': ['#F0F8FF', '#0A1628'],   // [light, dark] tuple
    '--radius-container': '12px',                   // static value
  },
});

```

### Step 2: Wrap Your Application with the Provider

Import the `Theme` component from `@astryxdesign/core/theme/Theme` and wrap your application tree. Pass the theme instance and a mode value:

```tsx
import {Theme} from '@astryxdesign/core/theme/Theme';
import {useState} from 'react';

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

  return (
    <Theme theme={oceanTheme} mode={mode}>
      <YourAppContent />
    </Theme>
  );
}

```

### Step 3: Implement Mode Switching

Update the `mode` prop to trigger theme changes. The provider automatically synchronizes the document root and CSS `color-scheme`:

```tsx
<button onClick={() => setMode(m => (m === 'dark' ? 'light' : 'dark'))}>
  Toggle Light/Dark
</button>

```

## Using the ThemeSwitcher Component

For rapid prototyping, Astryx provides a `ThemeSwitcher` component generated by the CLI at [`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). This component implements the toggle logic internally:

```tsx
import ThemeSwitcher from '@astryxdesign/cli/assets/templates/blocks/components/Theme/ThemeSwitcher.tsx';

export function Header() {
  return (
    <header>
      <h1>My Astryx App</h1>
      <ThemeSwitcher />
    </header>
  );
}

```

Note that [`packages/core/src/theme/themeRegistry.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/themeRegistry.ts) maintains a server-safe registry for theme lookups during SSR and React Server Components, ensuring consistent initial renders.

## Summary

- **Define tokens as tuples**: Use `[lightValue, darkValue]` arrays in `defineTheme` to generate CSS `light-dark()` functions automatically.
- **Leverage native CSS**: The browser handles mode switching without JavaScript runtime overhead once tokens are defined.
- **Use the `<Theme>` provider**: Pass `'light'`, `'dark'`, or `'system'` to the `mode` prop in [`packages/core/src/theme/Theme.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/Theme.tsx) to synchronize the document root.
- **Sync document attributes**: The `useRootThemeSync` hook sets `data-theme` and `data-astryx-theme` attributes for portal compatibility.
- **Reference CLI templates**: Use [`ThemeSwitcher.tsx`](https://github.com/facebook/astryx/blob/main/ThemeSwitcher.tsx) from the CLI assets for quick implementation of toggle functionality.

## Frequently Asked Questions

### How does Astryx handle dark mode without JavaScript runtime checks?

Astryx compiles `[light, dark]` token tuples into the CSS `light-dark()` function during the build process. As shown in [`packages/core/src/theme/defineTheme.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/defineTheme.ts), the `resolveTokenValue` function generates `light-dark(lightValue, darkValue)` strings that the browser evaluates natively based on the `color-scheme` property, eliminating the need for JavaScript to toggle styles manually.

### What is the difference between `data-theme` and `data-astryx-theme` attributes?

The `data-theme` attribute controls the browser's native color scheme (light/dark) and drives the `light-dark()` CSS function resolution. The `data-astryx-theme` attribute, set by `useRootThemeSync` in [`packages/core/src/theme/Theme.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/Theme.tsx), enables scoped CSS selectors to reach elements outside the React tree, such as portals or third-party widgets, ensuring consistent theming across the entire document.

### Can I use system preferences with Astryx theme tokens?

Yes. Pass `mode="system"` to the `<Theme>` component. This sets `colorScheme: 'light dark'` in the wrapper styles, allowing the browser to automatically choose between light and dark values based on the user's operating system preferences. The `light-dark()` CSS function will select the appropriate token value from your `[light, dark]` tuples accordingly.

### Where does Astryx store theme definitions for server-side rendering?

Theme registrations are maintained in [`packages/core/src/theme/themeRegistry.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/themeRegistry.ts). This server-safe registry allows React Server Components and SSR environments to look up theme tokens by name without accessing browser-only APIs, ensuring that `data-astryx-theme` attributes and CSS variables are correctly injected during the initial render.