Theme System

Theme provider, custom themes, theme build for production/SSR, light/dark mode, and component style overrides.

Quick Start

Basic theme setup (runtime injection)
tsx
import {Theme} from '@astryxdesign/core';
import {defaultTheme} from '@astryxdesign/theme-default';
function App() {
return (
<Theme theme={defaultTheme}>
<YourApp />
</Theme>
);
}
Optimized setup (pre-built CSS)
tsx
import {Theme} from '@astryxdesign/core';
import {defaultTheme} from '@astryxdesign/theme-default/built';
import '@astryxdesign/theme-default/theme.css';
function App() {
return (
<Theme theme={defaultTheme}>
<YourApp />
</Theme>
);
}
The default import uses runtime style injection, which works everywhere with no build step. The /built import skips injection and relies on the pre-compiled CSS file for better performance and SSR support.

Available Themes

ThemeImportDescription
Defaultimport {defaultTheme} from '@astryxdesign/theme-default'Blue accent, system fonts, light/dark
Neutralimport {neutralTheme} from '@astryxdesign/theme-neutral'Grayscale, shadcn-inspired
Brutalistimport {brutalistTheme} from '@astryxdesign/theme-brutalist'Zero radius, monospace, heavy borders
All theme packages export from two subpaths: - @astryxdesign/theme-{name}: source theme (runtime injection) - @astryxdesign/theme-{name}/built: pre-built theme (pair with theme.css)

Theme Props

PropTypeDefaultDescription
themeDefinedThemeTheme object (required)
mode'system' | 'light' | 'dark''system'Color mode. system follows OS preference.
childrenReactNodeApp content

Creating a Custom Theme

Use the CLI wizard (recommended) or create manually with defineTheme. Only override tokens that differ from defaults; omitted tokens use the design system defaults.
Scaffold with CLI
bash
npx astryx theme

defineTheme

defineTheme creates a theme from token overrides and optional scale configs. Scale configs generate tokens from parameters. Explicit token overrides always take precedence over scale-generated values.
defineTheme with scale configs
tsx
import {defineTheme} from '@astryxdesign/core/theme';
const myTheme = defineTheme({
name: 'my-theme',
color: { accent: '#7B61FF', neutralStyle: 'cool' },
typography: {
scale: { base: 14, ratio: 1.2 },
body: { family: 'Inter', fallbacks: '-apple-system, sans-serif' },
},
radius: { base: 4, multiplier: 1 },
motion: { fast: 175, medium: 410, ratio: 0.75 },
tokens: {
// Explicit overrides take precedence over scale-generated values
'--color-accent': ['#7B61FF', '#9B85FF'],
},
components: {
button: { 'variant:primary': { color: 'white' } },
},
});
ConfigGeneratesParameters
color--color-accent, --color-background-*, --color-text-*, --color-border, etc.accent (hex), neutralStyle? (warm|cool|neutral), contrast? (standard|high)
typography.scale--text-heading-*-size/weight/leading, --text-body-size/weight/leadingbase (px), ratio
typography.body/heading/code--font-family-body, --font-family-heading, --font-family-codefamily, fallbacks?, url?, weight?
radius--radius-inner, --radius-element, --radius-container, --radius-page, --radius-chatbase (px), multiplier (0–2)
motion--duration-fast-min/fast/fast-max, --duration-medium-min/medium/medium-maxfast (ms), medium (ms), ratio, easing?

Extending a Theme

extends lets you derive a new theme from an existing one, inheriting its tokens, component overrides, icons, and fonts. Only specify what you want to change; everything else carries over from the base theme.
Extending the default theme
tsx
import {defineTheme} from '@astryxdesign/core/theme';
import {defaultTheme} from '@astryxdesign/theme-default';
import {myIcons} from './icons';
const brandTheme = defineTheme({
name: 'brand',
extends: defaultTheme,
icons: myIcons,
tokens: {
'--color-accent': ['#7B61FF', '#9B85FF'],
},
});
FieldMerge behavior
tokensBase tokens are copied first, then child tokens override on top.
componentsDeep-merged: child component rules override matching keys from the base.
iconsShallow-merged: child icons override matching names from the base.
fontsBase fonts included first, then child fonts appended.
typography, motion, radius, colorChild config replaces base entirely (these are scale inputs, not additive).

Component Style Overrides

The components field in defineTheme uses semantic component keys and style keys — not raw CSS selectors. Use base for all instances, variant:value or stateName for specific props/states, and let the theme pipeline choose the underlying selector. For raw external CSS escape hatches, prefer the data-attribute selector surface documented in astryx docs styling.
Component overrides with standard CSS
tsx
components: {
// Standard CSS properties are expanded automatically.
// borderRadius also sets the internal radius var for concentric math.
// padding on container components (card, section, dialog) expands to layout tokens.
card: {
base: { borderRadius: '20px', padding: '24px' },
},
button: {
base: { borderRadius: '9999px', textTransform: 'uppercase' },
'variant:ghost': { borderWidth: '2px', borderStyle: 'solid' },
},
// Some components have public CSS vars for properties that don't map
// to standard CSS. Set these directly.
button: {
base: { '--button-press-scale': 'scale(0.95)' },
},
}
Run npx astryx component <Name> to see a component's theming targets, public CSS variables, and which standard CSS properties are supported.
  • Write standard CSS properties (borderRadius, padding); the pipeline expands them into internal vars.
  • Set public CSS vars directly when no standard property equivalent exists.
  • Set private CSS vars (prefixed --_) directly. Use standard CSS properties instead. astryx theme build will error.

Custom Variants

Themes can add new prop values to any component. Any prop:value key where the value isn't a built-in gets treated as a new variant. Use astryx theme build to generate TypeScript augmentations for type safety.
Adding custom variants
tsx
components: {
button: {
// Override an existing variant
'variant:secondary': { backgroundColor: 'rgba(0,0,0,0.06)' },
// Add a new variant — generates type augmentation on build
'variant:primary-muted': {
backgroundColor: 'light-dark(#F2F4F6, #28292C)',
color: 'var(--color-text-primary)',
},
},
banner: {
// Any extensible prop axis works — not just variant
'status:neutral': {
backgroundColor: 'var(--color-muted)',
color: 'var(--color-text-secondary)',
},
},
}
After building, the new values are type-safe in JSX:
Using custom variants
tsx
// TypeScript knows about 'primary-muted' after astryx theme build
<Button variant="primary-muted" label="Save draft" />
<Banner status="neutral" title="Note" />
Custom variants only work when the theme that defines them is active. The component's variant map is extended via module augmentation, with no changes to the component source needed.

Building Themes for Production

npx astryx theme build compiles a defineTheme file into production-ready artifacts. Recommended for SSR apps (Next.js, Remix) where styles must be present on first paint.
Build a theme
bash
npx astryx theme build ./src/themes/ocean.ts
This generates the following files alongside the source:
FileDescription
ocean.cssPre-compiled CSS with token overrides, component overrides, and prose element styles in @scope rules
ocean.jsES module exporting the theme object with __built: true and pre-resolved token values. Also re-exports the icon registry if the source theme declares one.
ocean.d.tsTypeScript declarations for the theme and icon registry exports
ocean.variants.d.ts(Optional) Module augmentations for custom component prop values found in the theme's component overrides
The __built: true flag tells Theme to skip runtime <style> injection; the CSS file handles it.
Using a custom built theme
tsx
import {oceanTheme} from './themes/ocean';
import './themes/ocean.css';
<Theme theme={oceanTheme}>
<App />
</Theme>

Runtime vs Built Themes

Themes work in two modes:
Runtime (source)Built
Import (published theme)@astryxdesign/theme-{name}@astryxdesign/theme-{name}/built + theme.css
Import (custom theme)defineTheme() directlyBuilt .js + .css from npx astryx theme build
How it worksuseInsertionEffect injects <style> at hydrationPre-compiled .css file loaded with the page
Component overridesInjected client-onlyIn static CSS: present during SSR
SSR safeTokens yes, component overrides flash on hydrationFully SSR safe: no flash
Best forDev, prototyping, client-only SPAsProduction, SSR apps (Next.js, Remix)
  • Use the /built subpath + theme.css for production SSR apps.
  • Use runtime themes during development for fast iteration.
  • Run npx astryx theme build for custom themes to get the built artifacts.
  • Use runtime themes in production SSR apps; component overrides will flash on hydration.
  • Import /built without the CSS file; component overrides won't apply.

Light/Dark Mode

Use [light, dark] tuples in token values for automatic mode switching. Use mode='system' (default) on Theme to follow OS preference.
Light/dark tuple
tsx
'--color-accent': ['#0064E0', '#2694FE'],
// ^light ^dark
Toggle with a button
tsx
const [mode, setMode] = useState<'light' | 'dark'>('light');
<Theme theme={myTheme} mode={mode}>
<Button
label={mode === 'light' ? 'Switch to Dark' : 'Switch to Light'}
onClick={() => setMode(m => (m === 'light' ? 'dark' : 'light'))}
/>
</Theme>;

Nesting Themes

Wrap different sections in separate <Theme> providers.
Dark sidebar with light content
tsx
<Theme theme={lightTheme} mode="light">
<Layout
header={<LayoutHeader>...</LayoutHeader>}
start={
<Theme theme={darkTheme} mode="dark">
<LayoutPanel>{/* Dark sidebar */}</LayoutPanel>
</Theme>
}
content={<LayoutContent>{/* Light content */}</LayoutContent>}
/>
</Theme>

Token Utilities

Use xdsTokenVar() when a non-StyleX styling library wants a CSS variable reference, and resolveXDSThemeTokens() when JavaScript needs token values for a specific theme and mode without React context.
CSS var references for styling-library configs
ts
import {xdsTokenVar, xdsTokenVars} from '@astryxdesign/core/theme/tokens';
const pandaOrEmotionTheme = {
colors: {
text: xdsTokenVar('--color-text-primary'),
surface: xdsTokenVars['--color-background-surface'],
},
spacing: {
4: xdsTokenVars['--spacing-4'],
},
};
Resolve token values without a hook
ts
import {resolveXDSThemeTokens} from '@astryxdesign/core/theme/tokens';
import {defaultTheme} from '@astryxdesign/theme-default';
const lightTokens = resolveXDSThemeTokens(defaultTheme, {mode: 'light'});
const chartTheme = {
textColor: lightTokens['--color-text-primary'],
seriesColor: lightTokens['--color-data-categorical-blue'],
};
The @astryxdesign/core/theme/tokens subpath is server-safe and does not require React. The main @astryxdesign/core/theme barrel also re-exports these helpers for client code that already imports theme APIs.

useTheme Hook

useTheme() uses the same token resolution as resolveXDSThemeTokens(), but reads the nearest Theme and effective color mode from React context and media query state. Use it inside client components for SVG, canvas, charts, maps, and third-party configuration objects that need token values in JavaScript instead of var(...) references.
Access resolved token values in React
tsx
import {useMemo} from 'react';
import {useTheme} from '@astryxdesign/core/theme';
function ChartConfig() {
const {mode, tokens} = useTheme();
const options = useMemo(
() => ({
mode,
textColor: tokens['--color-text-primary'],
gridColor: tokens['--color-border'],
seriesColor: tokens['--color-data-categorical-blue'],
}),
[mode, tokens],
);
return <Chart options={options} />;
}
Prefer CSS variables, StyleX token imports, xstyle, or className for ordinary styling. To change the theme or mode, manage state at the app level and pass it to <Theme>.See npx astryx docs styling-libraries for styling-library interop and npx astryx docs tokens for the full token reference.