# Themes and dark mode

- Page: https://jiguang132.com/resources/astryx/docs/themes
- Resource: Astryx (https://jiguang132.com/resources/astryx.md)
- All guides: https://jiguang132.com/resources/astryx/docs.md

Astryx 0.7.1: Themes and dark mode.


## Theme System

How to use and create themes: see Use a theme for applying themes, and Author a theme for creating and customizing them.

### Wrap your app in a theme

Install a theme, import it with the CLI, and wire the generated module once. For the full guide — available themes, integration themes, dark mode, nested themes, and production builds — see `use-a-theme`.

```tsx
import {Theme} from '@astryxdesign/core';
import {themes, defaultThemeSlug} from './astryx-themes';

function App() {
  return (
    <Theme theme={themes[defaultThemeSlug]}>
      <YourApp />
    </Theme>
  );
}
```

### Create a custom theme

Use `defineTheme` with token overrides, scale configs, and component style overrides. For the full guide — palette generation, extending themes, adaptations, custom variants, and production builds — see `author-a-theme`.

```bash
astryx theme add stone   # use an existing theme
astryx theme eject stone # fork its source to customize
astryx theme build src/themes/stone/stoneTheme.ts
```

For a new coordinated color system, run `astryx theme palette generate palette.config.json --out palette.generated.ts`. Review and commit the generated palette module and receipt, import its `palette` export, then reference stable stops from semantic tokens. Regenerating an accepted palette updates every mapped token that keeps those references; generation does not rewrite the theme.

```tsx
import {defineTheme} from '@astryxdesign/core/theme';
import {palette} from './palette.generated';

const myTheme = defineTheme({
  name: 'my-theme',
  tokens: {
    '--color-background-surface': [
      palette.neutral.light[100],
      palette.neutral.dark[15],
    ],
  },
});
```

### Dark mode

Use [light, dark] tuples in token values for automatic mode switching. Use mode='system' (default) on Theme to follow OS preference. Full guide: `use-a-theme`.

```tsx
'--color-accent': ['#0064E0', '#2694FE'],
//                   ^light     ^dark
```

### Integration themes

Install an integration and Astryx discovers its themes automatically. Import one with `astryx theme add <name> --package <integration>`. Use `theme eject` only for an independent source fork. For the full walkthrough, see `use-a-theme`.
## Use a theme

Apply a theme to your app: install, import with the CLI, wire the generated module, switch dark mode, nest themes, and choose runtime or built for production.

### Wrap your app in a theme

```bash
npm install @astryxdesign/theme-neutral
astryx theme add neutral
```

```tsx
import {Theme} from '@astryxdesign/core';
import {themes, defaultThemeSlug} from './astryx-themes';

function App() {
  return (
    <Theme theme={themes[defaultThemeSlug]}>
      <YourApp />
    </Theme>
  );
}
```

```tsx
import {useState} from 'react';
import {Theme} from '@astryxdesign/core';
import {themes, type ThemeSlug} from './astryx-themes';

const [slug, setSlug] = useState<ThemeSlug>('neutral');
const app = <Theme theme={themes[slug]}><YourApp /></Theme>;
```

`theme add` records an installed package theme and regenerates `src/astryx-themes.ts` or `.js` with its built module, production CSS, and optional font CSS. In a project without `src`, the module is at the project root. The first imported theme becomes the default. `theme use <slug>` changes that default, and `theme remove <slug>` removes a non-default theme.

Customize a package theme with `defineTheme({extends: importedTheme, ...})`, then build and add that local theme. Use `theme eject` only when you want an independent source fork that no longer receives the package owner's updates. For the full authoring guide, see `author-a-theme`.

### Migrating Earlier Theme Copies

A theme copied by the released `theme add` stays app source. The upgrade does not move, delete, or rewrite it. It only writes the missing same-stem descriptor with `maintained: false`, so the copy becomes a local theme.

```bash
astryx upgrade --from 0.6.4 --path . --apply
```

Before the upgrade runs, theme commands skip a descriptor-less copy in `src/themes`. `theme list` and doctor name it as unmigrated and show the upgrade command. A script that meant to copy source now runs `theme eject` with the same arguments. A script that meant to make the app use a theme runs `theme add`.

`ASTRYX_THEME` is no longer read. Run `theme add <slug>` and `theme use <slug>` to choose the default in a generated app theme module. When that module exists, component metadata reads its recorded default theme. Without the module, the released `package.json#astryx.theme` lookup keeps its meaning.

### Available Themes

Install the theme package you want with `npm install @astryxdesign/theme-{name}`, then import its slug with `theme add <slug>`. The CLI imports the package's built outputs for you.

| Theme | Add command | Description |
| --- | --- | --- |
| Neutral | astryx theme add neutral | Muted, minimal aesthetic with Figtree typography. A good starting point. |
| Butter | astryx theme add butter | Golden, buttery surfaces with blue accents; Sarina + Outfit type. |
| Chocolate | astryx theme add chocolate | Warm brown tones and cozy beige; Fraunces + Albert Sans type. |
| Gothic | astryx theme add gothic | Dark-only atmospheric theme; deep blue-gray surfaces, distressed display type. |
| Matcha | astryx theme add matcha | Earthy greens; DM Sans + Playwrite US Trad type. |
| Stone | astryx theme add stone | Warm stone and slate tones; Montserrat + Figtree type. |
| Y2K | astryx theme add y2k | Playful Y2K pop; periwinkle body, holographic accents, Poppins + Crimson Text. |

Every first-party package exports its built theme at `@astryxdesign/theme-{name}/built`, production CSS at `/theme.css`, and font loading CSS at `/fonts.css`. `theme add` writes those imports into the generated app module.

### Theme Props

`<Theme>` takes `theme` (required), `mode` (`'system'` by default, or `'light'`/`'dark'`), and `children`. For every prop, run `astryx component Theme`.

### Using a Theme from an Integration

Install the integration as a direct dependency. Astryx discovers its themes and guides without an `astryx.config` entry. A theme can be added only when the installed package exports its built module and production stylesheet.

```bash
npm install @astryxdesign/core @acme/brand-integration
astryx theme list --package @acme/brand-integration
astryx docs brand-theme
astryx theme add ocean --package @acme/brand-integration
```

`theme add` keeps the owner package in the app record and imports its built module, stylesheet, and optional font stylesheet. Package updates continue to reach the app. Run `theme eject ocean --package @acme/brand-integration` only to copy the source and descriptor into `src/themes/ocean` as an independent local fork.

### Dark mode

Use [light, dark] tuples in token values for automatic mode switching. Use mode='system' (default) on Theme to follow OS preference.

```tsx
'--color-accent': ['#0064E0', '#2694FE'],
//                   ^light     ^dark
```

```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>;
```

To create a theme with custom dark mode colors, see `author-a-theme`.

### Nested themes

Wrap different sections in separate `<Theme>` providers.

```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>
```

### 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() directly | Built .js + .css from `astryx theme build` |
| How it works | useInsertionEffect injects <style> at hydration | Pre-compiled .css file loaded with the page |
| Component overrides | Injected client-only | In static CSS: present during SSR |
| SSR safe | Tokens yes, component overrides flash on hydration | Fully SSR safe: no flash |
| Best for | Authoring input for `astryx theme build` | App wiring, in development and production |

- Use `theme add` for production apps; it wires the built module and CSS.
- Import `themes` and `defaultThemeSlug` from the generated module once, and keep that wiring in development too.
- While you edit a local theme, run `astryx theme build --watch` so its built files stay current.
- Run `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.
- Import package theme source into app runtime code.
- Hand-edit the generated theme module; `theme add`, `theme remove`, and `theme use` regenerate it.

To build a custom theme for production, see the Build section of `author-a-theme`.
## 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

Use an installed built theme as the base for ordinary customization. Import it into your source and pass it as `extends` to `defineTheme({extends: importedTheme, ...})`. Build the result, then add the local slug. Only eject when you need to own and maintain a full source fork.

```bash
astryx theme list
astryx theme add stone
astryx theme eject stone
astryx theme build src/themes/stone/stoneTheme.ts
astryx theme add stone
```

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 `use-a-theme`. To generate colors from seed values, see the Palette section below. Use `theme eject` only when you want an independent source fork that no longer receives the package owner’s updates.

### 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 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 explicit token overrides and optional scale configs. Existing themes may use `color` to expand an accent seed into neutral, surface, text, border, and related values; it is not an accent-only edit. Keep that configuration when preserving existing output. For new complete palettes, generate and review a committed palette module, then import its `palette` export and reference stable stops from semantic tokens. This keeps mapped roles coherent when the accepted palette is regenerated; generation itself does not rewrite the theme. Other scale configs generate typography, radius, and motion values from parameters. Explicit token overrides always take precedence over scale-generated values, token by token. When an existing `color` config is in use, overriding `--color-accent` in `tokens` re-points `--color-accent-muted`, `--color-text-accent`, and `--color-icon-accent`, but `--color-on-accent` stays baked from the `color.accent` seed. Prefer a `[light, dark]` tuple for `color.accent` over overriding `--color-accent` alone. localTokens accepts any valid CSS custom-property name; prefixes do not establish ownership.

```tsx
import {defineTheme} from '@astryxdesign/core/theme';
import {palette} from './palette.generated';

const myTheme = defineTheme({
  name: 'my-theme',
  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: {
    '--color-background-surface': [
      palette.neutral.light[100],
      palette.neutral.dark[15],
    ],
  },
});
```

| Config | Generates | Parameters |
| --- | --- | --- |
| color (existing themes) | --color-accent, --color-background-*, --color-text-*, --color-border, etc. | Supported broad scale: accent? (hex or [light, dark] tuple), neutralStyle? (warm\|cool\|neutral), contrast? (standard\|high). Use theme palette generate for new complete palettes. |
| 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 `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.

```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'],
  },
});
```

| 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. A base that is not a theme (most often an import that missed) throws when `defineTheme` runs, rather than producing a theme that silently inherits nothing.

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 `styling-advanced`.

```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.

- 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.
- Set a public CSS var the component does not define. It compiles to CSS that never applies, and the build does not warn. Take the names from `astryx component <Name>`.

### 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.

```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)',
    },
  },
}
```

```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.

```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'}},
      },
    ],
  },
});
```

| 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. Breakpoint configuration alone emits no CSS.

### 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.

A rule may replace a theme-local token only when the exact name is already enrolled by root `localTokens` or an enrolled base. Component writes in a rule are validated exactly like root `components`: same targets, axes, and value domains. The one addition is that a rule may not be the only place a custom value is enrolled: a value that is valid only because a theme enrolls it generates unconditional type augmentation, so declare it on the root theme first and let rules restyle it. Built-in values need no root declaration. When rules can match together, their ordered portable and local token writes are validated as one effective graph; any reachable cycle fails before CSS is emitted.

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.

```bash
astryx 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 |

The `__built: true` flag tells Theme to skip runtime `<style>` injection; the CSS file handles it. Load the generated CSS wherever you load the module.

```tsx
import {Theme} from '@astryxdesign/core';
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 runtime intentionally trusts `__built: true` and will not repair stale CSS from an older build. The build also warns when the theme names font families it does not load. See `font-setup` for the full recipe.

For the runtime vs built tradeoff, see the Runtime vs Built section of `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 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
```

In the example above, the generated theme imports `./icons.mjs` from `dist`. If the second command is skipped, `theme build` can still succeed, but loading or bundling the generated module fails because `dist/icons.mjs` is missing. `--icons-specifier` changes the emitted import; it does not create or verify the target file. Match the specifier to a module that resolves from the generated JS file.

Without `--icons-specifier`, the detected source import specifier is emitted unchanged. In the default flow without `--out`, a bundler can resolve an extensionless `./icons` to the neighboring `icons.tsx` source, but Node ESM does not perform that lookup and reports `ERR_MODULE_NOT_FOUND`. Moving the output with `--out` also changes where relative imports resolve from.

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, beside the root source.

```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. Themes are registered by name when created with `defineTheme()`; call `registerTheme(theme)` for prebuilt or object-literal themes that need name-based SSR lookup.

```ts
import {tokenVar, tokenVars} from '@astryxdesign/core/theme/tokens';

const pandaOrEmotionTheme = {
  colors: {
    text: tokenVar('--color-text-primary'),
    surface: tokenVars['--color-background-surface'],
  },
};
```

```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'],
};
```

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.

For styling library interop patterns, see `styling-libraries`.

### useTheme Hook

`useTheme()` reads the nearest Theme and effective color mode from React context. Prefer `dataVars` for CSS-capable SVG and DOM values. Use this hook for canvas, charts, maps, third-party configuration objects, or an SVG API that requires a concrete JavaScript color instead of a `var(...)` reference.

```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 `use-a-theme` for the provider setup, and `tokens` for the full token reference.

Pinned install:

```bash
npm install @astryxdesign/core@0.7.1 @astryxdesign/theme-neutral@0.7.1 @stylexjs/stylex@0.19.0
```

---

Index of all JGuang resources: https://jiguang132.com/llms.txt
