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

> Discover how Astryx theming leverages CSS custom properties for dynamic light/dark modes and theme inheritance. Explore runtime injection and build-time compilation.

- Repository: [Meta/astryx](https://github.com/facebook/astryx)
- Tags: deep-dive
- Published: 2026-07-15

---

**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`](https://github.com/facebook/astryx/blob/main/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`](https://github.com/facebook/astryx/blob/main/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`](https://github.com/facebook/astryx/blob/main/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`](https://github.com/facebook/astryx/blob/main/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.

```typescript
// 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.

```typescript
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`](https://github.com/facebook/astryx/blob/main/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`](https://github.com/facebook/astryx/blob/main/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.