How the Astryx Theme Provider Handles Runtime vs Pre‑built CSS

The Astryx Theme component injects CSS style tags dynamically at runtime for developer‑defined themes, but skips injection entirely and only synchronizes HTML attributes when using pre‑built themes compiled via the CLI.

The Theme provider in the facebook/astryx repository serves as the entry point for applying design system tokens to React applications. It operates in two distinct modes—runtime injection for dynamic themes and static optimization for pre‑built themes—controlled by the internal __built flag generated by the defineTheme utility.

Architecture of the Theme Provider

The provider distinguishes between runtime and pre‑built CSS through a boolean flag set during theme definition. When you call defineTheme(), the resulting object includes metadata that tells the provider whether to inject styles dynamically or rely on existing static CSS files.

In packages/core/src/theme/Theme.tsx, the component uses useThemeStyleInjection (lines 97‑176) to manage CSS insertion. This hook checks the theme.__built property (line 113) before performing any DOM operations. If the flag is present, the provider skips injection entirely and proceeds only to attribute synchronization.

Runtime CSS Injection Mode

When using a theme created directly with defineTheme() without the CLI build step, the provider enters runtime injection mode. This approach generates CSS during component execution and inserts it into the document head.

Style tag generation occurs inside a useInsertionEffect hook to ensure styles exist before painting. The provider creates two distinct <style> elements:

  • One targeting @layer reset for prose defaults
  • One targeting @layer astryx‑theme for component overrides

Each style tag receives a unique identifier based on the theme name and mode. The injection logic resides in packages/core/src/theme/Theme.tsx lines 122‑133, where the system also emits a development warning urging developers to use pre‑built themes for production performance.

// packages/core/src/theme/Theme.tsx (conceptual)
useInsertionEffect(() => {
  if (theme.__built) return;
  
  const styleId = `astryx-theme-${theme.name}`;
  // Inject @layer reset and @layer astryx-theme styles
  document.head.appendChild(styleTag);
}, [theme]);

When the component unmounts, the provider automatically removes these tags and deregisters the theme from the internal injectedThemes tracking set.

Pre‑built CSS Mode

Pre‑built themes bypass runtime injection entirely. When you run npx astryx theme build <file>, the CLI generates a CSS file and a JavaScript module that sets theme.__built = true.

In this mode, useThemeStyleInjection detects the built flag (line 113 in packages/core/src/theme/Theme.tsx) and returns immediately without creating DOM elements. The provider assumes the host application has already loaded the compiled CSS—typically imported from node_modules/@astryxdesign/theme-{name}/theme.css.

Root synchronization still occurs regardless of the mode. The first Theme component in the tree (identified via ThemeNestingContext) executes useRootThemeSync (lines 182‑214) to write data-theme and data-astryx-theme attributes to the <html> element. This enables browser‑native color scheme handling and ensures scoped CSS reaches portals.

Root Theme Synchronization

The synchronization mechanism distinguishes between root and nested themes. Only the root provider updates the document element:

  • data-theme: Set to light, dark, or removed for system mode
  • data-astryx-theme: Set to the theme's name attribute

This implementation in packages/core/src/theme/Theme.tsx (lines 182‑208) ensures that scrollbars, form controls, and date pickers respect the current color scheme. Nested themes skip this sync to prevent conflicting attribute writes while still providing theme context to their subtrees.

Practical Implementation Examples

Defining a Runtime Theme

Create a theme using defineTheme from packages/core/src/theme/defineTheme.ts:

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

export const ocean = defineTheme({
  name: 'ocean',
  tokens: {
    '--color-accent': ['#0077B6', '#48CAE4'],
    '--color-bg': ['#E0FBFC', '#1D3557'],
  },
  components: {
    button: {base: {borderRadius: '4px'}},
  },
  icons: oceanIcons,
});

Using Runtime Injection

Apply the theme without pre‑building:

import {Theme} from '@astryxdesign/core/theme';
import {ocean} from './theme';

function App() {
  return (
    <Theme theme={ocean} mode="system">
      <YourApp />
    </Theme>
  );
}

The provider injects the generated CSS on the first render.

Building a Pre‑built Theme

Compile the theme via CLI:

npx astryx theme build src/theme/ocean.ts

This creates:

  • node_modules/@astryxdesign/theme-ocean/built/index.js (with __built: true)
  • node_modules/@astryxdesign/theme-ocean/theme.css

Using Pre‑built CSS

Import the CSS and use the built theme:

import {Theme} from '@astryxdesign/core/theme';
import {oceanTheme} from '@astryxdesign/theme-ocean/built';
import '@astryxdesign/theme-ocean/theme.css';

function App() {
  return (
    <Theme theme={oceanTheme} mode="dark">
      <YourApp />
    </Theme>
  );
}

No runtime injection occurs; the provider only synchronizes HTML attributes.

Nesting Themes

Override themes for specific subtrees:

<Theme theme={lightTheme}>
  <Header />
  <Theme theme={darkTheme} mode="dark">
    <Modal /> {/* Uses dark mode while page stays light */}
  </Theme>
</Theme>

The inner Theme detects nesting via ThemeNestingContext and skips root attribute synchronization.

Summary

  • Runtime injection generates CSS dynamically via useInsertionEffect when theme.__built is undefined, inserting styles into @layer reset and @layer astryx‑theme blocks.
  • Pre‑built themes set __built: true via the CLI, causing the provider to skip injection and assume static CSS is already loaded.
  • Root synchronization occurs only for the first Theme in the tree, writing data-theme and data-astryx-theme to the <html> element to support browser‑native color schemes.
  • Nested themes consume context without modifying document attributes, enabling isolated theme overrides.
  • The defineTheme utility in packages/core/src/theme/defineTheme.ts creates theme objects, while the CLI build command generates optimized static assets.

Frequently Asked Questions

How does the Theme provider know whether to inject CSS at runtime?

The provider checks the theme.__built boolean flag in packages/core/src/theme/Theme.tsx (line 113). If this flag is present and true, useThemeStyleInjection returns immediately without creating style tags. If false or undefined, the hook generates CSS strings and inserts them into the document head via useInsertionEffect.

What is the performance impact of runtime CSS injection?

Runtime injection executes JavaScript to generate and insert styles during the component lifecycle, which can cause layout shifts and increase bundle size. The provider emits a development warning (lines 122‑133 in packages/core/src/theme/Theme.tsx) recommending pre‑built themes for production, where CSS is loaded via static files and parsed natively by the browser.

Why does the root Theme sync attributes to the HTML element?

The root provider executes useRootThemeSync (lines 182‑214 in packages/core/src/theme/Theme.tsx) to set data-theme and data-astryx-theme on the <html> tag. This enables browser‑native color scheme support for UI elements like scrollbars and form controls, and ensures scoped CSS reaches elements outside the React tree such as portals and modals.

Can I nest multiple Theme providers in the same application?

Yes. The Theme component uses ThemeNestingContext to detect when it is rendered inside another Theme. Nested providers skip root attribute synchronization and CSS injection if the parent already handled it, while still providing theme context to their descendants. This allows isolated theme overrides for specific components or sections.

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 →