Theme System
XDSTheme 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, which works everywhere with no build step. The 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>);}
/built import skips injection and relies on the pre-compiled CSS file for better performance and SSR support.Available Themes
| 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 |
@xds/theme-{name}: source theme (runtime injection)
- @xds/theme-{name}/built: pre-built theme (pair with theme.css)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 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
bashnpx xds 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
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 Overrides
Thecomponents 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 xds docs styling.Component overrides with standard CSS
Run 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)' },},}
npx xds 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.
xds theme buildwill error.
Custom Variants
Themes can add new prop values to any component. Anyprop: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, with 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.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 |
__built: true flag tells XDSTheme to skip runtime <style> injection; the CSS file handles it.Using a custom built theme
tsximport {oceanTheme} from './themes/ocean';import './themes/ocean.css';<XDSTheme theme={oceanTheme}><App /></XDSTheme>
Runtime vs Built Themes
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 buildfor 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 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 Themes
Wrap 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 tsximport {useXDSTheme} from '@xds/core';function MyComponent() {const ctx = useXDSTheme();// ctx.theme — the XDSDefinedTheme object// ctx.mode — 'system' | 'light' | 'dark'return null;}
npx xds docs styling for component-level customization (xstyle, className, rest props). See npx xds docs tokens for the full token reference.