# How to Create and Build Custom Astryx Themes Using the `defineTheme` Function

> Learn to build custom Astryx themes with the defineTheme function. Inject themes at runtime or pre-compile for production static CSS. Elevate your Astryx application's look and feel.

- Repository: [Meta/astryx](https://github.com/facebook/astryx)
- Tags: how-to-guide
- Published: 2026-07-02

---

**Use `defineTheme` from `@astryxdesign/core/theme` to transform a configuration object into a `DefinedTheme`, then either inject it at runtime with the `<Theme>` component or pre-compile it with `astryx theme build` for production static CSS.**

The `defineTheme` function is the core API in the **facebook/astryx** repository for creating design systems. It converts JavaScript or TypeScript theme configurations into structured themes that support automatic light/dark mode resolution, component style overrides, and optional build-time optimization.

## Understanding the `defineTheme` Architecture

According to the source code in [`packages/core/src/theme/defineTheme.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/defineTheme.ts), the `defineTheme` function accepts a `DefineThemeInput` object and returns a `DefinedTheme`. This process handles token resolution—including tuple-based light/dark values—and generates CSS that can either be injected at runtime or emitted as static files by the CLI.

### Core Theme Components

A complete theme definition includes several distinct systems:

- **Tokens**: Flat CSS custom properties (e.g., `--color-accent`, `--spacing-2`) that accept either string values or tuples `['#fff', '#000']` that resolve to `light-dark(#fff, #000)`
- **Typography, Motion, and Radius**: Configuration objects that automatically expand into full token scales via internal utilities like `expandTypeScale`, `expandMotionScale`, and `expandRadiusScale`
- **Component Overrides**: A `ComponentStyleMap` that maps component names to style keys, supporting `base` styles, variant selectors (`prop:value`), and pseudo-class keys (`:hover`, `:focus-visible`)
- **On-Media Overrides**: The `onDark` and `onLight` fields (handled in [`packages/core/src/theme/onMediaTokens.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/onMediaTokens.ts)) that supply secondary tokens when a subtree renders on a contrasting surface

### Runtime vs. Build Modes

Astryx supports two consumption patterns. In **runtime mode**, `defineTheme` executes in the browser and the `<Theme>` component (documented in `packages/core/src/theme/Theme.doc.mjs`) injects a `<style>` tag with generated CSS. In **build mode**, the `astryx theme build` command (implemented in `packages/cli/src/commands/build-theme.mjs`) pre-compiles the theme into three artifacts: [`theme.css`](https://github.com/facebook/astryx/blob/main/theme.css), [`theme.js`](https://github.com/facebook/astryx/blob/main/theme.js) (with `__built: true`), and type definitions.

## Creating Themes with `defineTheme`

### Basic Token Configuration

At minimum, a theme requires a name and a tokens map. Token values can be simple strings for static values or tuples for automatic light/dark mode switching.

```typescript
import { defineTheme } from '@astryxdesign/core/theme';

export const oceanTheme = defineTheme({
  name: 'ocean',
  tokens: {
    '--color-accent': ['#0077B6', '#48CAE4'],
    '--color-background-surface': ['#F0F8FF', '#0A1628'],
    '--radius-container': '16px',
  },
});

```

The function automatically resolves the tuple syntax to CSS `light-dark()` functions, enabling automatic theme switching without media queries.

### Typography, Motion, and Radius Scales

Beyond raw tokens, you can define systematic scales that generate consistent spacing, timing, and type values.

```typescript
import { defineTheme } from '@astryxdesign/core/theme';

export const marbleTheme = defineTheme({
  name: 'marble',
  typography: {
    scale: { base: 14, ratio: 1.2 },
    body: { family: 'Geist', fallbacks: '-apple-system, sans-serif' },
    heading: { family: 'Geist', fallbacks: '-apple-system, sans-serif' },
  },
  motion: { fast: 100, medium: 250, slow: 600, ratio: 0.8 },
  radius: { base: 4, multiplier: 0 },
  tokens: { '--color-accent': '#0064E0' },
});

```

The configuration above triggers the internal expansion utilities to generate the full token set from these base ratios.

### Component Style Overrides

The `components` field accepts a map of component names to style definitions, allowing you to override default component styles within your theme scope.

```typescript
import { defineTheme } from '@astryxdesign/core/theme';

export const neonTheme = defineTheme({
  name: 'neon',
  tokens: { '--color-accent': '#ff00ff' },
  components: {
    button: {
      base: { fontWeight: '600', backgroundColor: 'var(--color-accent)' },
      ':hover': { filter: 'brightness(1.2)' },
      'variant:secondary': { backgroundColor: '#222' },
      'size:sm': { padding: '4px 8px' },
    },
  },
});

```

These styles are automatically scoped under the theme's `data-astryx-theme` attribute, ensuring they only apply when the theme is active.

### Handling Dark Surfaces with `onDark` and `onLight`

Use `onDark` and `onLight` fields to specify alternate token and component configurations for subtrees that render on contrasting backgrounds.

```typescript
import { defineTheme } from '@astryxdesign/core/theme';

export const glassTheme = defineTheme({
  name: 'glass',
  tokens: { '--color-background-surface': '#fff' },
  onDark: {
    tokens: { '--color-background-surface': '#111' },
    components: {
      toast: { base: { backgroundColor: 'var(--color-background-surface)' } },
    },
  },
});

```

When wrapped with `<MediaTheme surface="dark">`, the system automatically applies these overrides from the `onDark` configuration.

## Building Themes for Production

### The CLI Build Process

For production applications and SSR frameworks like Next.js or Remix, pre-build your themes using the CLI command documented in `packages/cli/docs/theme.doc.mjs`:

```bash
npx astryx theme build ./packages/themes/y2k/src/y2kTheme.ts -o ./dist/y2k.css

```

This command generates three output files:
1. [`theme.css`](https://github.com/facebook/astryx/blob/main/theme.css) – Static CSS ready to link in your HTML `<head>`
2. [`theme.js`](https://github.com/facebook/astryx/blob/main/theme.js) – The `DefinedTheme` object with `__built: true` flag
3. [`theme.d.ts`](https://github.com/facebook/astryx/blob/main/theme.d.ts) – TypeScript declarations for the theme structure

### Integrating Built Themes in React

After building, import the CSS file directly and pass the built theme object to the `<Theme>` component. The component detects the `__built: true` flag and skips runtime injection, avoiding hydration mismatches.

```tsx
import { Theme } from '@astryxdesign/core/theme';
import { y2kTheme } from '../dist/y2k.js';
import '../dist/y2k.css';

export const App = () => (
  <Theme theme={y2kTheme}>
    <YourApp />
  </Theme>
);

```

## Complete Working Example

The **Y2K** theme in [`packages/themes/y2k/src/y2kTheme.ts`](https://github.com/facebook/astryx/blob/main/packages/themes/y2k/src/y2kTheme.ts) demonstrates the full API surface, combining syntax highlighting, icon registries, and component overrides.

```typescript
import { defineTheme, defineSyntaxTheme } from '@astryxdesign/core/theme';
import { y2kIconRegistry } from './icons';

const y2kSyntax = defineSyntaxTheme({ /* syntax tokens */ });

export const y2kTheme = defineTheme({
  name: 'y2k',
  typography: { scale: { base: 16, ratio: 1.25 }, /* ... */ },
  radius: { base: 4, multiplier: 0 },
  motion: { fast: 100, medium: 250, slow: 600, ratio: 0.8 },
  syntax: y2kSyntax,
  tokens: { /* color and spacing tokens */ },
  icons: y2kIconRegistry,
});

```

This file serves as the reference implementation for production-grade theme definitions in the Astryx ecosystem.

## Summary

- **`defineTheme`** in [`packages/core/src/theme/defineTheme.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/defineTheme.ts) converts configuration objects into `DefinedTheme` instances with automatic CSS generation
- **Token tuples** like `['#fff', '#000']` automatically resolve to `light-dark()` CSS functions for automatic mode switching
- **Component overrides** support `base` styles, pseudo-classes, and prop-based variants through the `ComponentStyleMap` structure
- **`onDark`/`onLight`** fields handle contrasting surfaces via the logic in [`packages/core/src/theme/onMediaTokens.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/theme/onMediaTokens.ts)
- **`astryx theme build`** pre-compiles themes into static CSS and JavaScript modules, recommended for production and SSR applications
- Built themes carry the `__built: true` flag, which tells the `<Theme>` component to skip runtime style injection

## Frequently Asked Questions

### What is the difference between runtime and built themes in Astryx?

Runtime themes execute `defineTheme` in the browser, injecting CSS via a `<style>` tag when the `<Theme>` component mounts. Built themes are pre-compiled using the CLI into static [`theme.css`](https://github.com/facebook/astryx/blob/main/theme.css) and [`theme.js`](https://github.com/facebook/astryx/blob/main/theme.js) files. The built version provides first-paint CSS and works with server-side rendering, while the runtime version is better for prototyping or user-editable themes.

### How do I handle light and dark mode in Astryx themes?

Pass tuples as token values: `['lightValue', 'darkValue']`. The `defineTheme` function automatically converts these to CSS `light-dark(lightValue, darkValue)` functions. Alternatively, use the `onDark` or `onLight` fields to specify complete alternate token maps and component styles for specific subtrees that render on contrasting backgrounds.

### Where should I place my custom theme files in an Astryx project?

Theme definitions typically live in a `packages/themes/` directory or within your application's `src/themes/` folder. The file should export the result of `defineTheme`, which you then build using `npx astryx theme build <path>` to generate the distribution files required for production.

### Can I override styles for specific component variants in my theme?

Yes. The `components` field in `DefineThemeInput` accepts a map where keys are component names and values include `base` styles, pseudo-class keys like `:hover`, and variant selectors formatted as `prop:value`. These overrides are scoped to the theme's `data-astryx-theme` attribute to prevent leakage.