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

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, 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) 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, 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.

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.

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.

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.

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:

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

This command generates three output files:

  1. theme.css – Static CSS ready to link in your HTML <head>
  2. theme.js – The DefinedTheme object with __built: true flag
  3. 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.

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 demonstrates the full API surface, combining syntax highlighting, icon registries, and component overrides.

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 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
  • 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 and 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.

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 →