How Astryx Theming Works with CSS Custom Properties: A Complete Guide

Astryx converts TypeScript design tokens into CSS custom properties that are injected at runtime or compiled at build time, enabling automatic light/dark mode switching and theme inheritance without JavaScript overhead.

Astryx, an open-source design system by Meta (facebook/astryx), leverages CSS custom properties to create a type-safe, composable theming layer. The system transforms static token definitions into dynamic CSS variables using StyleX, allowing components to respond to theme changes instantly. This architecture separates design decisions from component logic while maintaining full TypeScript autocompletion for all theme values.

Token Architecture in tokens.stylex.ts

All design tokens originate in packages/core/src/theme/tokens.stylex.ts. This file declares core tokens—covering color, spacing, size, radius, shadow, motion, and typography—as plain objects where each key follows the --token-name syntax.

The file exports token maps like colorVars and spacingVars via stylex.defineVars, which creates a mapping between TypeScript constants and CSS custom properties. For example, --color-accent, --spacing-4, and --radius-container are defined here and made available as typed variables throughout the system.

Theme Creation with defineTheme

The public API for creating themes is defineTheme in packages/core/src/theme/defineTheme.ts. This function accepts a configuration object containing token values that can be either static strings or light/dark tuples.

When you call defineTheme, the input values are merged with default tokens and converted to CSS custom-property assignments. The function generates the final CSS rules—using var(--token) for consumption—and prepares them for injection or compilation. For tokens specified as arrays, the system automatically wraps them in the native light-dark() CSS function.

Automatic Light/Dark Mode Handling

Astryx handles dark mode without JavaScript runtime switching by utilizing the native CSS light-dark() function. When a token value is provided as a tuple [lightValue, darkValue] in defineTheme, the system wraps these values according to lines 11–14 of defineTheme.ts.

This means the same CSS custom property works for both color schemes automatically. The browser applies the appropriate value based on the user's system preferences or the color-scheme property, eliminating flash-of-unstyled-content issues common to JavaScript-based theme switching.

Runtime Injection vs. Build-Time Compilation

Astryx supports two modes for delivering CSS custom properties to the browser:

Unbuilt mode: When using the runtime <Theme> component from packages/core/src/theme/index.ts, defineTheme generates the CSS dynamically and injects it into a <style data-astryx-theme="my-theme"> element when the component mounts. This is ideal for development or server-side rendered applications.

Built mode: The CLI command astryx theme build pre-compiles the theme CSS to a static file. In this mode, the <Theme> component only sets the data-astryx-theme attribute on the <html> element, and the CSS custom properties are loaded via a traditional stylesheet link. Both modes use identical CSS custom property names, ensuring consistent component behavior.

Theme Extension and Composition

Themes in Astryx are composable through the extends property. When defining a new theme, you can pass an existing DefinedTheme object as the base, then override specific tokens.

The system copies the base theme's CSS custom properties and applies your overrides, generating a new set of assignments that replace only the values you change. This creates efficient theme variants without duplicating the entire token set.

// Extend an existing theme and override specific tokens
import {neutralTheme} from '@astryxdesign/theme-neutral';
import {defineTheme} from '@astryxdesign/core/theme';

export const brandTheme = defineTheme({
  name: 'brand',
  extends: neutralTheme,
  tokens: {
    '--color-accent': '#FF4500',
    '--radius-container': ['12px', '14px'],
  },
});

Consuming CSS Custom Properties in Components

Components consume theme values by referencing the CSS custom properties directly or through StyleX helpers. Because variables are defined in the root of the theme's style block, any descendant component can access them automatically.

import * as stylex from '@stylexjs/stylex';
import {colorVars} from '@astryxdesign/core/theme';

const badgeStyles = stylex.create({
  root: {
    backgroundColor: `var(${colorVars['--color-accent']})`,
    padding: 'var(--spacing-2)',
  },
});

You can also use the raw CSS variable names (e.g., var(--color-accent)) in standard CSS or StyleX style objects, ensuring that a single theme object controls the entire UI surface.

Summary

  • Token definitions in tokens.stylex.ts map TypeScript constants to CSS custom properties using stylex.defineVars.
  • Theme creation via defineTheme merges tokens and generates CSS, supporting both static values and light/dark tuples.
  • Light/dark mode is handled natively through the CSS light-dark() function, requiring no JavaScript runtime logic.
  • Dual deployment modes support both runtime style injection and pre-compiled CSS via the astryx theme build CLI.
  • Theme inheritance allows extending base themes to create variants while overriding only specific tokens.
  • Component consumption uses standard var(--token) syntax, enabling universal access to theme values throughout the component tree.

Frequently Asked Questions

How does Astryx handle dark mode without JavaScript?

Astryx uses the native CSS light-dark() function, which is automatically applied when you provide token values as [light, dark] tuples in defineTheme. The browser switches between values based on the user's system preferences or the color-scheme CSS property, eliminating the need for JavaScript-based theme toggling and preventing flash-of-unstyled-content.

Can I extend existing themes in Astryx?

Yes. The defineTheme API accepts an extends property that references another DefinedTheme object. The system copies all CSS custom properties from the base theme and applies your overrides, generating a new theme that inherits defaults while replacing specific token values. This pattern is demonstrated in packages/themes/y2k/src/y2kTheme.ts.

What is the difference between runtime and build-time theming?

In runtime mode, defineTheme injects a <style> tag with your CSS custom properties when the <Theme> component mounts. In build-time mode, the astryx theme build CLI pre-generates a CSS file, and the <Theme> component only sets the data-astryx-theme attribute on the HTML element. Both approaches expose the same CSS custom property names to components.

How are tokens accessed in component styles?

Components access tokens through standard CSS var(--token-name) syntax. When using StyleX, you can import typed variable objects like colorVars from @astryxdesign/core/theme and interpolate them into your style definitions (e.g., `var(${colorVars['--color-accent']})`). This maintains type safety while outputting standard CSS custom property references.

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 →