Author a theme

Create, customize, and build a theme: defineTheme, palette generation, token overrides, component style overrides, custom variants, responsive adaptations, and production builds.

Custom themes

Start from a bundled theme or one contributed by an installed integration, or write one from scratch with defineTheme. theme list names each owner; when packages share a slug, pass --package. Only override tokens that differ from defaults; omitted tokens use the design system defaults.

Browse, then copy a theme in as editable source
bash
astryx theme list
astryx theme add stone
astryx theme add ocean --package @acme/themes

For an annotated map of the whole surface (every defineTheme field, the token families, and the component override syntax, each with the CLI command that prints its reference), run astryx theme template. It writes theme.template.ts into your project to read and copy from (astryx init --features theme writes it as part of project setup).

To apply a theme you created, see astryx docs use-a-theme. To generate colors from seed values, see the Palette section below.

Generate a palette

A theme needs dozens of related colors for backgrounds, borders, text, and states, in both light and dark mode. Rather than pick each by hand, name a few seed colors and generate the rest.

Minimal palette config (palette.config.json)
json
{"families": [{"id": "ocean", "seed": "#0074e2"}]}
Generate and preview
bash
# Print the palette without writing files
astryx theme palette generate palette.config.json
​
# Write palette + receipt, and open a visual preview
astryx theme palette generate palette.config.json \
--out src/themes/ocean/tokens/ocean.palette.ts \
--preview preview.html

The command writes a .ts file exporting 21 shades (stops 0–100) for both light and dark, plus a .receipt.json to regenerate later. Point your theme's tokens at the palette, then run defineTheme (see below).

For the full palette config fields and CLI flags, run astryx docs cli/commands/theme-palette-generate. For the integration-authoring walkthrough of connecting a palette to theme tokens, run astryx docs cli/integrations/building-blocks/themes/generate-a-palette.

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, token by token. localTokens accepts any valid CSS custom-property name; prefixes do not establish ownership. One caveat for the accent: overriding --color-accent in tokens re-points the reference tokens (--color-accent-muted, --color-text-accent, --color-icon-accent) but NOT --color-on-accent, which stays baked from the color.accent seed. To give each scheme its own accent with a consistent derived palette, pass a [light, dark] tuple to color.accent instead of overriding the token.

defineTheme with scale configs
tsx
import {defineTheme} from '@astryxdesign/core/theme';
​
const myTheme = defineTheme({
name: 'my-theme',
// accent: single hex, or [light, dark] tuple to seed each scheme separately
color: { accent: ['#7B61FF', '#9B85FF'], 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-background-body': ['#FFFFFF', '#0A0A0A'],
},
});
ConfigGeneratesParameters
color--color-accent, --color-background-*, --color-text-*, --color-border, etc.accent? (hex or [light, dark] tuple; omit for neutral-only), 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?

For dark mode tuples in token values, see the Dark mode section of astryx docs use-a-theme.

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 neutral theme
tsx
import {defineTheme} from '@astryxdesign/core/theme';
import {neutralTheme} from '@astryxdesign/theme-neutral';
import {myIcons} from './icons';
​
const brandTheme = defineTheme({
name: 'brand',
extends: neutralTheme,
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.
indicatorsShallow-merged: child indicators override matching names from the base.
onDark, onLightDeep-merged per surface: the base's resolved surface first, then the child's overrides.
typography, motion, radius, colorChild config replaces base entirely (these are scale inputs, not additive).
adaptationsWidth-breakpoint overrides merge by fixed name. Inherited ordered rules keep their relative order; child rules append and re-resolve against the child root axes.

Inheritance is resolved when the theme is defined, so an extended theme is flat: astryx theme build emits one self-contained stylesheet holding everything the child inherited, and the base theme's CSS does not need to be loaded next to it.

For the integration-specific guidance on mapping a palette to tokens when extending, see astryx docs cli/integrations/building-blocks/themes/define-the-theme.

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: {
card: {
base: { borderRadius: '20px', padding: '24px' },
},
button: {
base: {
borderRadius: '9999px',
textTransform: 'uppercase',
'--button-focus-offset': '3px',
},
'variant:ghost': { borderWidth: '2px', borderStyle: 'solid' },
},
}

Run astryx theme targets for every themeable key in the system (astryx theme targets <Name> to scope it, --json to lint a theme against it), and astryx component <Name> for one component's theming targets, public CSS variables, and which standard CSS properties are supported.

GuidancePractices
Do

Write standard CSS properties (borderRadius, padding); the pipeline expands them into internal vars.

Do

Set public CSS vars directly when no standard property equivalent exists.

Don't

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: {
'variant:secondary': { backgroundColor: 'rgba(0,0,0,0.06)' },
'variant:primary-muted': {
backgroundColor: 'light-dark(#F2F4F6, #28292C)',
color: 'var(--color-text-primary)',
},
},
banner: {
'status:neutral': {
backgroundColor: 'var(--color-background-muted)',
color: 'var(--color-text-secondary)',
},
},
}
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.

Theme Adaptations

Use adaptations for opt-in token, theme-local token, and component changes under viewport width, primary-pointer precision, contrast preference, or motion preference. Conditions in one when are ANDed. Rules are ordinary ordered objects, and later matching writes win.

Width and pointer adaptations
tsx
const acmeTheme = defineTheme({
name: 'acme',
adaptations: {
widthBreakpoints: {
sm: 640, md: 768, lg: 1024, xl: 1280, '2xl': 1536,
},
rules: [
{
when: {width: {below: 'md'}},
value: {tokens: {'--spacing-4': '12px'}},
},
{
when: {pointer: 'coarse'},
value: {tokens: {'--size-element-sm': '36px', '--size-element-md': '40px', '--size-element-lg': '44px'}},
},
],
},
});
ConditionValues
width.from / width.belowsm | md | lg | xl | 2xl
pointercoarse | fine
contrastmore | less | no-preference
motionreduce | no-preference

widthBreakpoints are fixed named start points. Defaults are 640 / 768 / 1024 / 1280 / 1536 CSS pixels. from includes its point; below excludes it.

Adaptation order and validation

Precedence follows rule order. Root theme values apply first, then every matching rule in declaration order. A later rule may deliberately restore a root value. onDark and onLight media-surface overrides apply after adaptations and win on the same leaf.

extends preserves the base rule order and appends child rules. Inherited conditions use the child's effective breakpoint map. An empty child rule is a no-op, not a removal operator.

Adaptations compile to CSS media queries with no resize listener or styling rerender. Runtime and astryx theme build use the same compiler, but only a built theme is present at first paint in an SSR app.

Build a theme

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
astryx theme build ./src/themes/ocean.ts
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.
ocean.d.tsTypeScript declarations for the theme and icon registry exports
ocean.variants.d.ts(Optional) Module augmentations for custom component prop values
Using a custom built theme
tsx
import {oceanTheme} from './themes/ocean';
import './themes/ocean.css';
​
<Theme theme={oceanTheme}>
<App />
</Theme>

After upgrading Astryx, rerun astryx theme build for every custom prebuilt theme. Deploy the regenerated files together. The build also warns when the theme names font families it does not load. See astryx docs typography for the full recipe.

For the runtime vs built tradeoff, see the Runtime vs Built section of astryx docs use-a-theme.

Built themes with an icon registry

theme build emits an icon import when it detects a named import used by the theme's icons: field. It does not compile that registry module. Move the registry to a separate module and use a named import.

Compiling the icon registry sidecar
bash
# Emit the built theme
astryx theme build ./src/themes/ocean.ts -o dist/theme.css --icons-specifier ./icons.mjs
​
# Compile the icon registry alongside it
esbuild src/themes/icons.tsx --bundle --format=esm --outfile=dist/icons.mjs \
--external:react --external:lucide-react --jsx=automatic

Keep react and the icon library external so the registry does not bundle its own copies of those dependencies.

Building a Theme Family

Use family mode when an app switches among one base theme and its selected descendants. The build writes one keyed CSS file containing every member, plus one keyed JavaScript module and one declaration file.

Build one family
bash
astryx theme build --family \
./src/themes/ocean.mjs \
./src/themes/ocean-calm.mjs \
./src/themes/ocean-calm-deep.mjs \
--family-key ocean-family

The family stylesheet eagerly downloads every selected member so first paint is complete. Switching members changes only the theme identity; it does not add, remove, or reorder stylesheets.

Token Utilities

Use tokenVar() when a non-StyleX styling library wants a CSS variable reference, and resolveThemeTokens() when JavaScript needs token values for a specific theme and mode without React context.

CSS var references for styling-library configs
ts
import {tokenVar, tokenVars} from '@astryxdesign/core/theme/tokens';
​
const pandaOrEmotionTheme = {
colors: {
text: tokenVar('--color-text-primary'),
surface: tokenVars['--color-background-surface'],
},
};
Resolve token values without a hook
ts
import {resolveThemeTokens} from '@astryxdesign/core/theme/tokens';
import {neutralTheme} from '@astryxdesign/theme-neutral';
​
const lightTokens = resolveThemeTokens(neutralTheme, {mode: 'light'});
const chartTheme = {
textColor: lightTokens['--color-text-primary'],
seriesColor: lightTokens['--color-data-categorical-blue'],
};

For styling library interop patterns, see astryx docs styling-libraries.

useTheme Hook

useTheme() reads the nearest Theme and effective color mode from React context. Use it inside client components for SVG, canvas, charts, maps, and third-party configuration objects that need token values in JavaScript.

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 for ordinary styling. See astryx docs use-a-theme for the provider setup, and astryx docs tokens for the full token reference.