---
title: Customization
description: "Theming with tokens, dark mode, per-instance overrides, and editing component source."
sidebar:
  order: 2
---

Components are copied into your project — you own the source. Customization
happens at four levels, from broadest to most targeted:

1. **Tokens** — retheme the whole app by editing one file.
2. **Themes** — swap token values for a subtree or the whole page.
3. **The `style` prop** — override any component instance from the outside.
4. **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, and
shadows. Every component reads from it — change a value and the whole
system follows:

```tsx
// lib/tokens.stylex.ts
export const colors = stylex.defineVars({
  // ← your brand color here: light side, dark side
  primary: 'light-dark(oklch(0.55 0.2 260), oklch(0.7 0.16 260))',
  primaryForeground: 'light-dark(oklch(0.985 0 0), oklch(0.205 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.

## Fonts

Components set no `font-family` of their own: they inherit the page's font,
so the face your app already uses reaches every control. Set it on `<html>`,
not on a wrapper, because dialogs, popovers, menus, and toasts portal to
`<body>` and inherit from there. The reset `init` writes gives `<html>` the
system sans stack, which is what components render in until you pick a font.

```tsx
// app/layout.tsx
import { Geist } from 'next/font/google';

const geist = Geist({ subsets: ['latin'] });

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={geist.className}>
      <body>{children}</body>
    </html>
  );
}
```

## Dark mode

Every color token is a `light-dark(light, dark)` pair, so dark mode needs no
theme: `colorScheme` from `lib/themes.ts` sets CSS `color-scheme` on `<html>`
and the browser picks a side of each token. It follows the OS by default, and
`data-theme="dark"` or `"light"` on `<html>` forces a mode. When you change a
color, edit both sides of its pair. The page background and text color
follow too: `page` from `lib/themes.ts` on `<body>` reads the `background` and
`foreground` tokens. See [Dark mode](/docs/dark-mode) for the toggle and the
no-flash setup.

## Themes

A theme overrides token values for a DOM subtree. Brand themes are
`stylex.createTheme` calls in `lib/themes.ts`, and their values can be
`light-dark()` pairs so one theme covers both modes:

```tsx
// lib/themes.ts
export const brandTheme = stylex.createTheme(colors, {
  primary: 'light-dark(oklch(0.55 0.2 260), oklch(0.7 0.16 260))',
  primaryForeground: 'light-dark(oklch(0.985 0 0), oklch(0.205 0 0))',
  ring: 'light-dark(oklch(0.55 0.2 260), oklch(0.7 0.16 260))',
});
```

A theme doesn't have to override every token; the ones it leaves out keep
their values from `tokens.stylex.ts`. Apply it to `<html>`, not a wrapper
`<div>`, because dialogs, popovers, menus, and toasts portal to `<body>` and
would escape a subtree theme:

```tsx
<html {...stylex.props(colorScheme, brandTheme)}>
```

Apply one theme per element. Two themes over the same tokens do not merge:
`stylex.props(themeA, themeB)` applies only `themeB`. A theme on a nested
element overrides the one above it, but only for content rendered inside that
element, so portaled popups still get the `<html>` theme.

madeui's own palette is neutral by default — `primary` is near-black ink — so
the accent is yours to pick rather than one you have to override.

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:

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

```tsx
// 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:

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

```tsx
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 everywhere | Edit `lib/tokens.stylex.ts` |
| Change the font everywhere | Set `font-family` on `<html>` |
| Turn on dark mode | `colorScheme` from `lib/themes.ts` on `<html>` |
| Add 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 |
