How to Implement Dark Mode Using Astryx Theming: A Complete Guide
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:
-
Themecomponent — 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 inpackages/core/src/theme/Theme.tsx. -
useThemehook — Reads the nearestThemeContextand returns the resolved token map and effective mode. Falls back to root HTML attributes when no provider is present. Found inpackages/core/src/theme/useTheme.ts. -
ThemeContext— Holds the theme object and requested mode (system,light,dark). Defined inpackages/core/src/theme/useTheme.ts. -
defineTheme— Creates aDefinedThemewith token overrides and optional component style overrides. Lives inpackages/core/src/theme/defineTheme.ts. -
useRootThemeSync— Internal hook insideTheme.tsxthat writesdata-themeanddata-astryx-themeto<html>, settingcolor-schemeCSS 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 selects the appropriate entry based on the effective mode.
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:
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 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":
<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 lang="en" data-theme="dark">
See the SSR comment in 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:
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:
setModeupdates themodepropThemecallsuseRootThemeSyncto update<html data-theme>useThemerecomputeseffectiveMode(resolving system preference if needed)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:
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.
Key Implementation Files
| File | Purpose |
|---|---|
packages/core/src/theme/Theme.tsx |
Provider component with root attribute sync and CSS injection |
packages/core/src/theme/useTheme.ts |
Hook for token resolution and effective mode detection |
packages/core/src/theme/tokens.ts |
Token definitions and resolveThemeTokens function |
packages/core/src/theme/defineTheme.ts |
Theme creation utility |
packages/cli/assets/templates/blocks/components/Theme/ThemeSwitcher.tsx |
Reference implementation for theme switching UI |
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
Themeprovider, passingmodeas"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, enablingcolor-schemefor native UI - Support SSR by setting
data-themeon 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) 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.
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 →