How to Create Custom Astryx Themes Beyond the Built-In Options

The recommended pattern for Astryx custom themes involves exporting a theme object that matches the built-in theme shape and passing it to the XDSTheme provider, without forking the library.

Astryx themes are CSS custom-property collections consumed by the design system's provider layer. Whether you need a corporate rebrand, dark mode variant, or specialized accessibility palette, you can create fully functional themes as separate packages or local modules. This guide demonstrates the official pattern used by the facebook/astryx source code for extending the theme system beyond neutral, stone, and other built-in options.

Core Concept: Theme as a Structured Object

Astryx treats themes as structured configuration objects rather than style overrides. The XDSTheme provider in packages/core/src/theme/ reads CSS custom properties at runtime, which means your theme only needs two components: a TypeScript/JavaScript object defining tokens, and a CSS file declaring the corresponding custom properties.

The createTheme helper from @astryxdesign/core/theme ensures your object conforms to the expected interface defined in packages/core/src/utils/themeProps.ts, which specifies required keys for colors, typography, spacing, and other design tokens.

Step-by-Step: Creating a Custom Astryx Theme

1. Scaffold Your Theme Package

Create a new package or local module that exports a theme object. The structure mirrors built-in themes like packages/themes/stone/:


my-custom-theme/
├── src/
│   ├── theme.ts          # Exports the theme object

│   └── built/
│       └── theme.css     # Generated CSS custom properties

├── package.json
└── README.md

Reference packages/themes/stone/README.md in the Astryx repository for the standard layout, including "Install", "Usage", and "Import paths" sections.

2. Define Tokens with createTheme

Use the exposed helper to build your theme object with the same keys as built-in themes:

// my-custom-theme/src/theme.ts
import {createTheme} from '@astryxdesign/core/theme';
import {stylex} from '@stylexjs/stylex';

export const myTheme = createTheme({
  // Colors – must match built-in theme keys
  color: {
    brand: '#0062FF',
    background: '#FAFAFA',
    textPrimary: '#111111',
    textSecondary: '#6B7280',
    border: '#E5E7EB',
    success: '#10B981',
    warning: '#F59E0B',
    error: '#EF4444',
  },

  // Typography
  fontFamily: {
    body: 'Inter, sans-serif',
    heading: 'Roboto, sans-serif',
    code: 'Source Code Pro, monospace',
  },

  // Spacing with StyleX variable definitions
  space: stylex.defineVars({ 
    sm: '4px', 
    md: '8px', 
    lg: '16px',
    xl: '24px',
    xxl: '32px',
  }),

  // Border radius
  radius: stylex.defineVars({ 
    small: '2px', 
    medium: '4px',
    large: '8px',
  }),
});

The createTheme helper validates against the interface in packages/core/src/utils/themeProps.ts, which defines the shape all themes must follow. Missing required keys will surface as TypeScript errors.

3. Generate or Write the CSS File

The CSS file declares custom properties that match your theme tokens. You have two options:

  • Build-generated: Run @astryxdesign/build (StyleX-based) to auto-generate CSS from your theme object
  • Hand-written: Create the file manually following the variable naming convention
/* my-custom-theme/src/built/theme.css (auto-generated or hand-written) */
:root {
  --color-brand: #0062FF;
  --color-background: #FAFAFA;
  --color-text-primary: #111111;
  --color-text-secondary: #6B7280;
  --color-border: #E5E7EB;
  --color-success: #10B981;
  --color-warning: #F59E0B;
  --color-error: #EF4444;
  
  --font-family-body: 'Inter, sans-serif';
  --font-family-heading: 'Roboto, sans-serif';
  --font-family-code: 'Source Code Pro, monospace';
  
  --space-sm: 4px;
  --space-md: 8px;
  --space-lg: 16px;
  --space-xl: 24px;
  --space-xxl: 32px;
  
  --radius-small: 2px;
  --radius-medium: 4px;
  --radius-large: 8px;
}

Import paths follow the pattern documented in packages/cli/assets/docs/theme.doc.mjs: base themes use @astryxdesign/theme-<name> while built/compiled assets use @astryxdesign/theme-<name>/built.

4. Apply with XDSTheme Provider

Wrap your application and pass the custom theme object. The provider reads CSS custom properties at runtime—no additional wrappers needed:

// App.tsx
import {XDSTheme} from '@astryxdesign/core/theme';
import {myTheme} from 'my-custom-theme/src/theme';

// Import base theme (optional) and your custom theme CSS
import '@astryxdesign/theme-neutral/theme.css';    // optional foundation
import 'my-custom-theme/src/built/theme.css';      // your custom properties

function App() {
  return (
    <XDSTheme theme={myTheme}>
      <YourApplication />
    </XDSTheme>
  );
}

The XDSTheme component in packages/core/src/theme/ handles runtime property resolution. Your theme object provides the token structure; the CSS file provides the actual computed values.

5. (Optional) Register for CLI Discovery

To enable astryx theme list to discover your theme, add an entry to the central registry:

// packages/core/src/theme/themeRegistry.ts
import {myTheme} from 'my-custom-theme/src/theme';

export const themeRegistry = {
  neutral: neutralTheme,
  stone: stoneTheme,
  dark: darkTheme,
  // ...built-in themes
  'my-custom': myTheme,   // ← register for CLI visibility
};

The themeRegistry.ts file maps string names to exported theme objects. Registration is only required for CLI integration—your theme functions fully without this step.

Distribution Options for Custom Themes

The decoupled architecture supports multiple distribution patterns:

Pattern Best For Implementation
NPM package Organization-wide sharing Publish to private or public registry; consumers import as @yourscope/theme-name
Git submodule Monorepo with shared themes Add as submodule; import via relative path or workspace protocol
Local folder Single application customization Co-locate in project; import from local path
Upstream contribution Themes suitable for core library Follow packages/themes/stone/ structure; submit PR to facebook/astryx

All patterns use identical import and consumption code—only the package resolution differs.

Key Source Files Reference

Understanding these files in the facebook/astryx repository ensures your theme remains compatible:

Common Pitfalls and Solutions

Missing CSS import: The theme object alone does not render styles—you must import the CSS file defining custom properties. The XDSTheme provider resolves values at runtime from the CSS cascade.

Mismatched keys: Token keys must exactly match the interface in themeProps.ts. Use createTheme to catch discrepancies at build time.

StyleX version conflicts: When using stylex.defineVars() for spacing or other tokens, ensure your package's @stylexjs/stylex version aligns with @astryxdesign/core's dependency range.

Summary

  • Custom Astryx themes are structured objects consumed by XDSTheme, not style overrides or forked components
  • Use createTheme from @astryxdesign/core/theme to ensure interface compliance with packages/core/src/utils/themeProps.ts
  • Ship a CSS file declaring custom properties—auto-generated via @astryxdesign/build or hand-written
  • Distribute as npm packages, git submodules, or local folders without core library modifications
  • Register in themeRegistry.ts only if CLI discovery via astryx theme list is required

Frequently Asked Questions

Do I need to fork Astryx to create a custom theme?

No. Themes are intentionally decoupled from core components. You create a standalone package or module that exports a theme object and CSS file, then pass that object to XDSTheme. The source code in facebook/astryx demonstrates this through the built-in theme packages, which follow the same external package pattern available to custom implementations.

What happens if my theme object is missing required keys?

TypeScript will surface errors at build time because createTheme validates against the interface in packages/core/src/utils/themeProps.ts. Runtime behavior depends on component implementation—some may fall back to defaults, others may render incorrectly. Always use createTheme rather than plain objects to ensure complete coverage.

Can I override only specific tokens from a built-in theme?

Yes. Import a base theme object, use object spread to modify specific tokens, and pass the result to createTheme. The CSS file approach also supports layering: import a base theme's CSS, then import your override CSS with redefined custom properties for selective replacement.

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 →