How Dark Mode Functions with the Astryx Theme System
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 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, 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. 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, 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 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:
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:
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:
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:
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)')inuseTheme.tsand memoized for stability. - Tokens resolve to concrete values through
resolveXDSTokenValueandresolveThemeTokensintokens.ts, supporting both tuple arrays and CSSlight-dark()functions. - Inverse surfaces are handled efficiently via
onMediaTokens.tswithout nested theme providers. - The
useTheme()hook exposes atoken(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 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →