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.
bashastryx theme listastryx theme add stoneastryx 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.
json{"families": [{"id": "ocean", "seed": "#0074e2"}]}
bash# Print the palette without writing filesastryx theme palette generate palette.config.json# Write palette + receipt, and open a visual previewastryx 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.
tsximport {defineTheme} from '@astryxdesign/core/theme';const myTheme = defineTheme({name: 'my-theme',// accent: single hex, or [light, dark] tuple to seed each scheme separatelycolor: { 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'],},});
| Config | Generates | Parameters |
|---|---|---|
| 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/leading | base (px), ratio |
| typography.body/heading/code | --font-family-body, --font-family-heading, --font-family-code | family, fallbacks?, url?, weight? |
| radius | --radius-inner, --radius-element, --radius-container, --radius-page, --radius-chat | base (px), multiplier (0–2) |
| motion | --duration-fast-min/fast/fast-max, --duration-medium-min/medium/medium-max | fast (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.
tsximport {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'],},});
| Field | Merge behavior |
|---|---|
| tokens | Base tokens are copied first, then child tokens override on top. |
| components | Deep-merged: child component rules override matching keys from the base. |
| icons | Shallow-merged: child icons override matching names from the base. |
| indicators | Shallow-merged: child indicators override matching names from the base. |
| onDark, onLight | Deep-merged per surface: the base's resolved surface first, then the child's overrides. |
| typography, motion, radius, color | Child config replaces base entirely (these are scale inputs, not additive). |
| adaptations | Width-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.
tsxcomponents: {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.
| Guidance | Practices |
|---|---|
| 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. |
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.
tsxcomponents: {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)',},},}
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.
tsxconst 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'}},},],},});
| Condition | Values |
|---|---|
| width.from / width.below | sm | md | lg | xl | 2xl |
| pointer | coarse | fine |
| contrast | more | less | no-preference |
| motion | reduce | 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.
bashastryx theme build ./src/themes/ocean.ts
| File | Description |
|---|---|
| ocean.css | Pre-compiled CSS with token overrides, component overrides, and prose element styles in @scope rules |
| ocean.js | ES module exporting the theme object with __built: true and pre-resolved token values. |
| ocean.d.ts | TypeScript declarations for the theme and icon registry exports |
| ocean.variants.d.ts | (Optional) Module augmentations for custom component prop values |
tsximport {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.
bash# Emit the built themeastryx theme build ./src/themes/ocean.ts -o dist/theme.css --icons-specifier ./icons.mjs# Compile the icon registry alongside itesbuild 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.
bashastryx 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.
tsimport {tokenVar, tokenVars} from '@astryxdesign/core/theme/tokens';const pandaOrEmotionTheme = {colors: {text: tokenVar('--color-text-primary'),surface: tokenVars['--color-background-surface'],},};
tsimport {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.
tsximport {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.