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:

  • Theme component — 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 in packages/core/src/theme/Theme.tsx.

  • useTheme hook — Reads the nearest ThemeContext and returns the resolved token map and effective mode. Falls back to root HTML attributes when no provider is present. Found in packages/core/src/theme/useTheme.ts.

  • ThemeContext — Holds the theme object and requested mode (system, light, dark). Defined in packages/core/src/theme/useTheme.ts.

  • defineTheme — Creates a DefinedTheme with token overrides and optional component style overrides. Lives in packages/core/src/theme/defineTheme.ts.

  • useRootThemeSync — Internal hook inside Theme.tsx that writes data-theme and data-astryx-theme to <html>, setting color-scheme CSS 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:

  1. setMode updates the mode prop
  2. Theme calls useRootThemeSync to update <html data-theme>
  3. useTheme recomputes effectiveMode (resolving system preference if needed)
  4. 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 Theme provider, passing mode as "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, enabling color-scheme for native UI
  • Support SSR by setting data-theme on 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →