Customization
Theming with tokens, dark mode, per-instance overrides, and editing component source.
Components are copied into your project — you own the source. Customization happens at four levels, from broadest to most targeted:
- Tokens — retheme the whole app by editing one file.
- Themes — swap token values for a subtree or the whole page (dark mode).
- The
styleprop — override any component instance from the outside. - The source itself — change a component’s anatomy or add variants.
Tokens
lib/tokens.stylex.ts is the single source of truth for colors, radii, fonts,
and shadows. Every component reads from it — change a value and the whole
system follows:
// lib/tokens.stylex.ts
export const colors = stylex.defineVars({
primary: 'oklch(0.55 0.2 260)', // ← your brand color here
primaryForeground: 'oklch(0.985 0 0)',
// ...
});
export const radius = stylex.defineVars({
md: '0.5rem', // ← sharper or rounder corners everywhere
// ...
});
Semantic names use the familiar convention (background, foreground,
primary, muted, accent, destructive, border, ring, …), so the
mental model transfers directly.
Colors are oklch() — perceptually uniform, so shifting lightness or chroma
behaves predictably, and any CSS color (hex, hsl space syntax) works too.
A few value-format rules keep files installable through the shadcn CLI (its
transformer rewrites some CSS-like strings): never use comma syntax like
rgba(0, 0, 0, 0.5); write black-with-alpha as oklch(0% 0 0deg / 50%)
(adjacent equal zeros get collapsed); keep shadow colors hex-alpha.
Themes and dark mode
A theme overrides token values for a DOM subtree. lib/themes.ts ships a
darkTheme built with stylex.createTheme:
// lib/themes.ts
export const darkTheme = stylex.createTheme(colors, {
background: 'oklch(0.145 0 0)',
foreground: 'oklch(0.985 0 0)',
// ...
});
Apply it to <html> — not a wrapper <div> — because dialogs, popovers,
menus, and toasts portal to <body> and would escape a subtree theme:
'use client';
useEffect(() => {
const { className } = stylex.props(dark && darkTheme);
document.documentElement.className = className ?? '';
}, [dark]);
Brand themes work the same way: create as many as you need with
stylex.createTheme(colors, { ... }) and apply per page, per tenant, or per
section. Themes can be nested — the closest one wins. See
Dark mode for the no-flash setup.
Accent themes compose
A theme doesn’t have to override every token. lib/themes.ts also ships
partial accent themes (violetTheme, emeraldTheme) that swap only the
brand tokens (primary, primaryForeground, ring). Partial themes compose
with full ones — later themes win per token:
// Dark mode with a violet brand color:
const { className } = stylex.props(darkTheme, violetTheme);
document.documentElement.className = className ?? '';
Add your own the same way:
export const brandTheme = stylex.createTheme(colors, {
primary: 'oklch(0.55 0.2 260)',
primaryForeground: 'oklch(0.985 0 0)',
ring: 'oklch(0.55 0.2 260)',
});
One thing themes are not: runtime-generated. StyleX resolves every theme
at compile time (that’s what keeps the CSS static and small), so a
“pick-a-color” theme builder must emit a createTheme call per choice rather
than compute values in the browser.
The style prop
Every styled component (and part) accepts style?: StyleXStyles. It is merged
last, so your overrides always win — StyleX resolves conflicts
deterministically at compile time. No tailwind-merge, no !important, no
specificity wars:
const styles = stylex.create({
pill: { borderRadius: '9999px' },
wide: { paddingInline: '3rem' },
});
<Button style={styles.pill}>Pill</Button>
<Button style={[styles.pill, styles.wide]}>Pill + wide</Button> // arrays compose
This is the right tool for one-off tweaks: a wider dialog, a full-width button, an extra margin. If you find yourself repeating the same override, promote it to a variant instead.
Editing the source
Components live in components/ui/ in your project. They are yours — edit
them. The most common edit is adding a variant:
// components/ui/button.tsx
export type ButtonVariant =
| 'primary'
| 'secondary'
| 'outline'
| 'ghost'
| 'destructive'
| 'success'; // 1. extend the type
const variants = stylex.create({
// ...existing variants...
success: { // 2. add the styles
backgroundColor: 'oklch(0.63 0.17 149)',
color: colors.primaryForeground,
},
});
// 3. done: <Button variant="success">Save</Button>
Sizes work the same way through the sizes map.
Styling Base UI state
Base UI mirrors every part’s state as a data attribute (data-checked,
data-highlighted, data-popup-open, data-starting-style, …), and StyleX
(0.18+) accepts attribute selectors as condition keys — so state styling is
declared inline, the Tailwind data-[state=open]: equivalent:
const styles = stylex.create({
root: {
backgroundColor: {
default: colors.input,
'[data-checked]': colors.primary, // like data-[state=checked]:bg-primary
},
},
});
<Switch.Root {...stylex.props(styles.root, style)} />
Compound states nest ('[data-side="bottom"]': { '[data-ending-style]': … })
or combine into one key ('[data-side="bottom"][data-ending-style]'). One
gotcha: a conditional custom property ('--x': { … }) must use
default: null and a var(--x, fallback) where it’s read — a non-null
default is emitted outside the CSS layer and would always win.
Interaction states that CSS can express directly (hover, focus, active) use the same conditional-value syntax:
backgroundColor: {
default: colors.background,
':hover': colors.accent,
},
outline: {
default: 'none',
':focus-visible': `2px solid ${colors.ring}`,
},
Where to make which change
| You want to… | Do this |
|---|---|
| Change the brand color / radius / font everywhere | Edit lib/tokens.stylex.ts |
| Add dark mode or a second brand look | stylex.createTheme in lib/themes.ts, apply to <html> |
| Tweak one instance | style prop |
| Reusable new look for a component | Add a variant in its source file |
| Change a component’s structure or behavior | Edit components/ui/<name>.tsx — you own it |