Theme SystemXDSTheme provider, custom themes, theme build for production/SSR, light/dark mode, and component style overrides.
Quick Start
Basic theme setup (runtime injection)
tsximport {XDSTheme} from '@xds/core';import {defaultTheme} from '@xds/theme-default';function App() {return (<XDSTheme theme={defaultTheme}><YourApp /></XDSTheme>);}
Optimized setup (pre-built CSS)
The default import uses runtime style injection — works everywhere, no build step. The `/built` import skips injection and relies on the pre-compiled CSS file for better performance and SSR support.tsximport {XDSTheme} from '@xds/core';import {defaultTheme} from '@xds/theme-default/built';import '@xds/theme-default/theme.css';function App() {return (<XDSTheme theme={defaultTheme}><YourApp /></XDSTheme>);}
Available Themes
All theme packages export from two subpaths:
- `@xds/theme-{name}` — source theme (runtime injection)
- `@xds/theme-{name}/built` — pre-built theme (pair with `theme.css`)
| Theme | Import | Description |
|---|---|---|
| Default | import {defaultTheme} from '@xds/theme-default' | Blue accent, system fonts, light/dark |
| Neutral | import {neutralTheme} from '@xds/theme-neutral' | Grayscale, shadcn-inspired |
| Brutalist | import {brutalistTheme} from '@xds/theme-brutalist' | Zero radius, monospace, heavy borders |
XDSTheme Props
| Prop | Type | Default | Description |
|---|---|---|---|
| theme | XDSDefinedTheme | — | Theme object (required) |
| mode | 'system' | 'light' | 'dark' | 'system' | Color mode. system follows OS preference. |
| children | ReactNode | — | App content |
Creating a Custom ThemeUse the CLI wizard (recommended) or create manually with defineTheme. Only override tokens that differ from defaults — omitted tokens use the XDS defaults.
Scaffold with CLI
bashnpx xds theme
defineThemedefineTheme 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
tsximport {defineTheme} from '@xds/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' } },},});
| Config | Generates | Parameters |
|---|---|---|
| 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/leading | base (px), ratio |
| typography.body/heading/code | --font-family-body, --font-family-heading, --font-family-code | family, fallbacks?, url?, weight? |
| radius | --radius-1 through --radius-4, --radius-container, --radius-page | base (px), multiplier (0–2) |
| motion | --duration-fast-min/fast/fast-max, --duration-medium-min/medium/medium-max | fast (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
tsximport {defineTheme} from '@xds/core/theme';import {defaultTheme} from '@xds/theme-default';import {myIcons} from './icons';const brandTheme = defineTheme({name: 'brand',extends: defaultTheme,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. |
| fonts | Base fonts included first, then child fonts appended. |
| typography, motion, radius, color | Child config replaces base entirely (these are scale inputs, not additive). |
Component Style OverridesThe `components` field in defineTheme targets stable `.xds-*` CSS class names. Use `base` for all instances, `variant:value` or `stateName` for specific states.
Component overrides with standard CSS
Run `npx xds component <Name>` to see a component’s theming targets, public CSS variables, and which standard CSS properties are supported.tsxcomponents: {// 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)' },},}
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. `xds theme build` will error.
Custom VariantsThemes 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 `xds theme build` to generate TypeScript augmentations for type safety.
Adding custom variants
After building, the new values are type-safe in JSX:tsxcomponents: {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)',},},}
Using custom variants
Custom variants only work when the theme that defines them is active. The component’s variant map is extended via module augmentation — no changes to the component source needed.tsx// TypeScript knows about 'primary-muted' after xds theme build<XDSButton variant="primary-muted" label="Save draft" /><XDSBanner status="neutral" title="Note" />
Building Themes for Production`npx xds 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.
The `__built: true` flag tells XDSTheme to skip runtime `<style>` injection — the CSS file handles it.
Build a theme
This generates the following files alongside the source:bashnpx xds 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. Also re-exports the icon registry if the source theme declares one. |
| ocean.d.ts | TypeScript 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 |
Using a custom built theme
tsximport {oceanTheme} from './themes/ocean';import './themes/ocean.css';<XDSTheme theme={oceanTheme}><App /></XDSTheme>
Runtime vs Built ThemesXDS themes work in two modes:
| Runtime (source) | Built | |
|---|---|---|
| Import (published theme) | @xds/theme-{name} | @xds/theme-{name}/built + theme.css |
| Import (custom theme) | defineTheme() directly | Built .js + .css from `npx xds 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 | Dev, prototyping, client-only SPAs | Production, 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 xds 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 ModeUse [light, dark] tuples in token values for automatic mode switching. Use mode='system' (default) on XDSTheme to follow OS preference.
Light/dark tuple
tsx'--color-accent': ['#0064E0', '#2694FE'],// ^light ^dark
Toggle with a button
tsxconst [mode, setMode] = useState<'light' | 'dark'>('light');<XDSTheme theme={myTheme} mode={mode}><XDSButtonlabel={mode === 'light' ? 'Switch to Dark' : 'Switch to Light'}onClick={() => setMode(m => (m === 'light' ? 'dark' : 'light'))}/></XDSTheme>;
Nesting ThemesWrap different sections in separate <XDSTheme> providers.
Dark sidebar with light content
tsx<XDSTheme theme={lightTheme} mode="light"><XDSLayoutheader={<XDSLayoutHeader>...</XDSLayoutHeader>}start={<XDSTheme theme={darkTheme} mode="dark"><XDSLayoutPanel>{/* Dark sidebar */}</XDSLayoutPanel></XDSTheme>}content={<XDSLayoutContent>{/* Light content */}</XDSLayoutContent>}/></XDSTheme>
useXDSTheme Hook
Access current theme
This is read-only. To change the theme/mode, manage state at the app level and pass it to <XDSTheme>.See `npx xds docs styling` for component-level customization (xstyle, className, rest props). See `npx xds docs tokens` for the full token reference.tsximport {useXDSTheme} from '@xds/core';function MyComponent() {const ctx = useXDSTheme();// ctx.theme — the XDSDefinedTheme object// ctx.mode — 'system' | 'light' | 'dark'return null;}