Theme SystemXDSTheme provider, custom themes, theme build for production/SSR, light/dark mode, and component style overrides.
Quick Start
Basic theme setup (runtime injection)
tsx
import {XDSTheme} from '@xds/core';
import {defaultTheme} from '@xds/theme-default';
function App() {
return (
<XDSTheme theme={defaultTheme}>
<YourApp />
</XDSTheme>
);
}
Optimized setup (pre-built CSS)
tsx
import {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>
);
}
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.
Available Themes
ThemeImportDescription
Defaultimport {defaultTheme} from '@xds/theme-default'Blue accent, system fonts, light/dark
Neutralimport {neutralTheme} from '@xds/theme-neutral'Grayscale, shadcn-inspired
Brutalistimport {brutalistTheme} from '@xds/theme-brutalist'Zero radius, monospace, heavy borders
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`)
XDSTheme Props
PropTypeDefaultDescription
themeXDSDefinedThemeTheme object (required)
mode'system' | 'light' | 'dark''system'Color mode. system follows OS preference.
childrenReactNodeApp 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
bash
npx 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
tsx
import {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' } },
},
});
ConfigGeneratesParameters
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/leadingbase (px), ratio
typography.body/heading/code--font-family-body, --font-family-heading, --font-family-codefamily, fallbacks?, url?, weight?
radius--radius-1 through --radius-4, --radius-container, --radius-pagebase (px), multiplier (0–2)
motion--duration-fast-min/fast/fast-max, --duration-medium-min/medium/medium-maxfast (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
tsx
import {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'],
},
});
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.
fontsBase fonts included first, then child fonts appended.
typography, motion, radius, colorChild 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
tsx
components: {
// 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)' },
},
}
Run `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 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
tsx
components: {
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)',
},
},
}
After building, the new values are type-safe in JSX:
Using custom variants
tsx
// TypeScript knows about 'primary-muted' after xds theme build
<XDSButton variant="primary-muted" label="Save draft" />
<XDSBanner 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 — no changes to the component source needed.
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
bash
npx xds theme build ./src/themes/ocean.ts
This generates the following files alongside the source:
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. Also re-exports the icon registry if the source theme declares one.
ocean.d.tsTypeScript 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
The `__built: true` flag tells XDSTheme to skip runtime `<style>` injection — the CSS file handles it.
Using a custom built theme
tsx
import {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() directlyBuilt .js + .css from `npx xds theme build`
How it worksuseInsertionEffect injects <style> at hydrationPre-compiled .css file loaded with the page
Component overridesInjected client-onlyIn static CSS — present during SSR
SSR safeTokens yes, component overrides flash on hydrationFully SSR safe — no flash
Best forDev, prototyping, client-only SPAsProduction, 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
tsx
const [mode, setMode] = useState<'light' | 'dark'>('light');
<XDSTheme theme={myTheme} mode={mode}>
<XDSButton
label={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">
<XDSLayout
header={<XDSLayoutHeader>...</XDSLayoutHeader>}
start={
<XDSTheme theme={darkTheme} mode="dark">
<XDSLayoutPanel>{/* Dark sidebar */}</XDSLayoutPanel>
</XDSTheme>
}
content={<XDSLayoutContent>{/* Light content */}</XDSLayoutContent>}
/>
</XDSTheme>
useXDSTheme Hook
Access current theme
tsx
import {useXDSTheme} from '@xds/core';
function MyComponent() {
const ctx = useXDSTheme();
// ctx.theme — the XDSDefinedTheme object
// ctx.mode — 'system' | 'light' | 'dark'
return null;
}
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.