# How Dark Mode Functions with the Astryx Theme System

> Discover how Astryx theme system implements dark mode efficiently. Learn how it resolves at runtime to support CSS and JS consumers without double renders.

- Repository: [Meta/astryx](https://github.com/facebook/astryx)
- Tags: deep-dive
- Published: 2026-07-02

---

**Astryx treats dark mode as a first-class color mode that resolves once at runtime, exposing values through a memoized token map that supports both CSS custom properties and JavaScript consumers without double renders.**

The facebook/astryx repository implements a deterministic theme system where dark mode is not merely a CSS override but a fundamental resolution stage in the token pipeline. Unlike frameworks that toggle class names or rely on media query overlays, Astryx computes concrete color values at the provider level, enabling consistent access across DOM elements, Canvas rendering, and SVG graphics. This architecture eliminates flash-of-unstyled-content issues and ensures server-safe rendering without hydration mismatches.

## Theme Provider Architecture and Mode Resolution

The `<Theme>` component exported from [`packages/core/src/theme/index.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/index.ts) accepts a `mode` prop that accepts three distinct values: `'light'`, `'dark'`, or `'system'`. When the consumer specifies `'system'`, the `useTheme` hook internally calls `useMediaQuery('(prefers-color-scheme: dark)')` to detect the operating system preference before determining the effective mode.

### Resolving System Preferences

The resolution logic lives in [`useTheme.ts`](https://github.com/facebook/astryx/blob/main/useTheme.ts), where the effective mode is memoized as either `'light'` or `'dark'` after evaluating the system preference. This memoization ensures that all child components receive a stable color mode for the lifetime of the theme context, preventing unnecessary re-renders when the token map remains valid.

## Token Resolution and Value Computation

Once the effective mode is determined, Astryx computes concrete CSS values through pure JavaScript functions defined in [`tokens.ts`](https://github.com/facebook/astryx/blob/main/tokens.ts). The system supports two primary token definition patterns: tuple arrays and CSS `light-dark()` functions.

### Tuple-Based Token Definitions

Design tokens can be defined as tuples containing light and dark values in the format `[lightValue, darkValue]`. The `resolveXDSTokenValue` function selects the appropriate index based on the effective mode, returning the concrete value immediately. This resolution happens once per theme change and is cached for the component tree lifecycle.

### The resolveThemeTokens Implementation

The `resolveThemeTokens` function builds a complete mapping of token names to resolved values. According to the source code in [`tokens.ts`](https://github.com/facebook/astryx/blob/main/tokens.ts), this function processes the entire token dictionary, applying mode-aware resolution to each entry. The resulting map contains final CSS values like `"#0064E0"` rather than variable references, enabling direct consumption by JavaScript-driven rendering targets such as WebGL or Canvas.

## Handling Inverse Surfaces with On-Media Tokens

For components rendered on opposing color schemes—such as a light button inside a dark card—Astryx provides the [`onMediaTokens.ts`](https://github.com/facebook/astryx/blob/main/onMediaTokens.ts) utility. This system supplies minimal token overrides including `color-scheme`, `--color-on-dark`, and `--color-accent` that flip specific values without requiring a nested `<Theme>` provider.

The `onMediaTokens.resolveOnMedia` function accepts a target mode and returns a sparse token object containing only the necessary overrides. This approach avoids the performance cost of mounting a second theme provider while maintaining visual consistency across surface boundaries.

## Accessing Dark Mode Values in Components

Components consume the resolved theme through the `useTheme()` hook, which returns a stable `token(name)` helper, the complete `tokens` map, and the determined `mode` string. This API supports both CSS-in-JS libraries and imperative rendering contexts like Canvas or SVG.

### Basic Provider Setup

Force dark mode regardless of system preference:

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

function App() {
  return (
    <Theme mode="dark">
      <Dashboard />
    </Theme>
  );
}

```

### Reading Token Values in JavaScript

Access concrete color values for data visualization or SVG rendering:

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

function Chart() {
  const { token, mode } = useTheme();

  const accent = token('--color-accent');
  const bg = token('--color-background-surface');

  return (
    <svg width="200" height="200" style={{ background: bg }}>
      <circle cx="100" cy="100" r="80" fill={accent} />
      <text x="100" y="105" textAnchor="middle" fill={token('--color-text-primary')}>
        {mode === 'dark' ? 'Dark' : 'Light'}
      </text>
    </svg>
  );
}

```

### CSS-in-JS Integration

Use the `tokenVar` helper to generate CSS custom property references:

```ts
import { tokenVar } from '@astryxdesign/core/theme';

const myStyle = {
  color: tokenVar('--color-text-primary'),
  backgroundColor: tokenVar('--color-background-surface'),
};

```

### Inverse Surface Overrides

Apply dark mode tokens to a specific surface within a light theme:

```tsx
import { Theme, onMediaTokens } from '@astryxdesign/core';

function DarkCard() {
  const darkOverrides = onMediaTokens.resolveOnMedia('dark', {
    tokens: { '--color-accent': ['#0064E0', '#A0C9FF'] },
  });

  return (
    <Theme mode="light">
      <div style={darkOverrides.tokens}>
        {/* content renders with dark color scheme */}
      </div>
    </Theme>
  );
}

```

## Performance Optimizations

The Astryx theme system guarantees single-pass resolution of dark mode values. Because the token map is constructed once per theme and mode combination and then memoized, components do not trigger additional renders when accessing color values. This architecture is purely client-side and requires no server-side rendering support for mode detection, as the initial resolution happens immediately after hydration using the `useMediaQuery` detection.

## Summary

- **Astryx resolves dark mode once at runtime** through the `<Theme>` provider, supporting explicit `'light'`, `'dark'`, or `'system'` selections.
- **System preferences are detected** via `useMediaQuery('(prefers-color-scheme: dark)')` in [`useTheme.ts`](https://github.com/facebook/astryx/blob/main/useTheme.ts) and memoized for stability.
- **Tokens resolve to concrete values** through `resolveXDSTokenValue` and `resolveThemeTokens` in [`tokens.ts`](https://github.com/facebook/astryx/blob/main/tokens.ts), supporting both tuple arrays and CSS `light-dark()` functions.
- **Inverse surfaces are handled efficiently** via [`onMediaTokens.ts`](https://github.com/facebook/astryx/blob/main/onMediaTokens.ts) without nested theme providers.
- **The `useTheme()` hook** exposes a `token(name)` helper that works across CSS, Canvas, and SVG contexts without additional DOM reads.

## Frequently Asked Questions

### How does Astryx detect system dark mode preferences?

When the `<Theme>` provider receives `mode="system"`, the `useTheme` hook executes `useMediaQuery('(prefers-color-scheme: dark)')` to query the OS-level preference. This boolean result is converted to either `'light'` or `'dark'` and stored as the effective mode for the entire component tree, ensuring consistent token resolution without hydration mismatches.

### Can Astryx themes support dark mode only without light variants?

Yes, themes can define tokens as single values rather than tuples when only one color scheme is supported. The [`gothicTheme.ts`](https://github.com/facebook/astryx/blob/main/gothicTheme.ts) example in `packages/themes/gothic/src/` demonstrates a theme that ships exclusively dark tokens, bypassing the tuple resolution logic entirely while maintaining compatibility with the `token()` API.

### What is the difference between `token()` and `tokenVar()` in dark mode?

The `token()` function returns the resolved concrete CSS value (e.g., `"#0064E0"`) for the current mode, suitable for JavaScript-based rendering or direct style assignment. The `tokenVar()` function returns the CSS custom property reference (e.g., `"var(--color-text-primary)"`), optimized for CSS-in-JS libraries that inject styles into the DOM. Both respect the effective mode determined by the `<Theme>` provider.

### How do on-media tokens handle nested color schemes without performance issues?

The `onMediaTokens.resolveOnMedia()` function defined in [`onMediaTokens.ts`](https://github.com/facebook/astryx/blob/main/onMediaTokens.ts) generates a sparse override object containing only the tokens that must flip for the opposing color scheme, such as `color-scheme` and surface colors. This approach avoids mounting a second `<Theme>` provider and the associated React context overhead, allowing components to render with inverted colors while maintaining the parent's memoization cache.