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 resetfor prose defaults - One targeting
@layer astryx‑themefor 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 tolight,dark, or removed forsystemmodedata-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
useInsertionEffectwhentheme.__builtis undefined, inserting styles into@layer resetand@layer astryx‑themeblocks. - Pre‑built themes set
__built: truevia the CLI, causing the provider to skip injection and assume static CSS is already loaded. - Root synchronization occurs only for the first
Themein the tree, writingdata-themeanddata-astryx-themeto the<html>element to support browser‑native color schemes. - Nested themes consume context without modifying document attributes, enabling isolated theme overrides.
- The
defineThemeutility inpackages/core/src/theme/defineTheme.tscreates 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →