How to Implement Dark Mode and Light Mode Switching with Astryx Theme Tokens
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
The foundation of mode switching resides in 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, the resolveTokenValue helper function detects array-based tokens and wraps them accordingly:
// 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 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:
// 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 implements the useRootThemeSync hook. This hook updates the <html> element's attributes to drive the light-dark() resolution:
// 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:
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:
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:
<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. This component implements the toggle logic internally:
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 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 indefineThemeto generate CSSlight-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 themodeprop inpackages/core/src/theme/Theme.tsxto synchronize the document root. - Sync document attributes: The
useRootThemeSynchook setsdata-themeanddata-astryx-themeattributes for portal compatibility. - Reference CLI templates: Use
ThemeSwitcher.tsxfrom 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, 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, 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. 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.
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 →