# Customization
nuka-ui is an opinionated library. It has strong opinions about accessibility, token structure, component API shape, and TypeScript correctness. Within those constraints it is designed to be practical to customize for real product work.
This guide covers every supported customization path, when to use each one, and where the hard boundaries are.
---
## The customization model
nuka-ui separates two concerns that are often conflated:
- **Theming** - changing colors, spacing, radius, and typography system-wide via CSS custom properties
- **Styling overrides** - adjusting individual component instances via `className`
These are different operations with different scopes. Theming affects everything in the tree. Styling overrides affect one instance. Neither requires touching library source.
A third path - **wrapping** - exists for cases where you need new named variants or intents that carry compound logic. This is always a build step in your codebase, not a library extension point.
---
## 1. Token overrides
The primary customization mechanism. All components reference semantic CSS tokens with the `--nuka-` prefix. Override those tokens and the components follow.
### How the token system works
nuka-ui uses a two-layer architecture:
```
--color-accent-500 <- primitive (raw oklch value, no prefix)
|
v
--nuka-accent-bg <- semantic (references primitive, --nuka- prefix)
|
v
bg-(--nuka-accent-bg) <- component (Tailwind class referencing semantic token)
```
Components only ever reference semantic tokens. Primitives are internal. This means you can retheme the library by overriding `--nuka-*` tokens without touching any component file.
### Changing the accent color
The accent color drives the `default` intent across primary, secondary, outline, ghost, and link variants on every component that uses the variant + intent pattern.
Example: switching the default slate accent to a vivid blue:
```css
[data-theme="light"] {
--nuka-accent-bg: oklch(44% 0.19 264);
--nuka-accent-bg-hover: oklch(37% 0.19 264);
--nuka-accent-bg-active: oklch(28% 0.18 264);
--nuka-accent-bg-subtle: oklch(96% 0.03 264);
--nuka-accent-border: oklch(70% 0.1 264);
--nuka-accent-text: oklch(44% 0.19 264);
}
[data-theme="dark"] {
--nuka-accent-bg: oklch(55% 0.19 264);
--nuka-accent-bg-hover: oklch(65% 0.18 264);
--nuka-accent-bg-active: oklch(72% 0.16 264);
--nuka-accent-bg-subtle: oklch(20% 0.06 264);
--nuka-accent-border: oklch(55% 0.19 264);
--nuka-accent-text: oklch(80% 0.12 264);
}
```
**Contrast is your responsibility.** nuka-ui ships with a verified accessible default (7.74:1 on white, WCAG AAA). When you override accent tokens, verify that your values maintain at least 4.5:1 for normal text and 3:1 for large text and UI components against the backgrounds they appear on.
### Changing feedback colors
Each intent (danger, success, warning) has five tokens: background, text, border, base, and foreground. The base token is used for filled/primary visual weight. The foreground (`-fg`) token is the text color on filled surfaces. The others are used for subtle, secondary, and outline treatments.
```css
[data-theme="light"] {
--nuka-danger-bg: oklch(95% 0.06 15);
--nuka-danger-text: oklch(42% 0.2 15);
--nuka-danger-border: oklch(75% 0.14 15);
--nuka-danger-base: oklch(56% 0.22 15);
--nuka-danger-fg: oklch(100% 0 0);
}
```
The same five-token pattern applies to `--nuka-success-*` and `--nuka-warning-*`. The `--nuka-warning-fg` uses dark text (neutral-900) because amber hues cannot pass 4.5:1 contrast with white.
### Changing dark theme surfaces
Dark theme surfaces use a separate set of primitives: `--color-neutral-dark-*`. These live on `:root` alongside other primitives, not inside `[data-theme="dark"]`. Override them on `:root` to retheme the dark surface hierarchy without affecting light mode.
| Primitive | Default | Role |
| ------------------------------------- | ---------------------- | -------------------------------- |
| `--color-neutral-dark-base` | `oklch(30% 0.012 257)` | Page background |
| `--color-neutral-dark-subtle` | `oklch(36% 0.014 257)` | Subtle surface (cards, sidebars) |
| `--color-neutral-dark-muted` | `oklch(38% 0.016 257)` | Muted/filled surface |
| `--color-neutral-dark-border` | `oklch(46% 0.016 257)` | Default border |
| `--color-neutral-dark-input` | `oklch(26% 0.012 257)` | Input background |
| `--color-neutral-dark-input-readonly` | `oklch(27% 0.012 257)` | Readonly input background |
Example: shifting the dark surface stack to a warmer tone:
```css
:root {
--color-neutral-dark-base: oklch(28% 0.015 45);
--color-neutral-dark-subtle: oklch(34% 0.017 45);
--color-neutral-dark-muted: oklch(36% 0.019 45);
--color-neutral-dark-border: oklch(44% 0.019 45);
--color-neutral-dark-input: oklch(24% 0.015 45);
--color-neutral-dark-input-readonly: oklch(25% 0.015 45);
}
```
These primitives are on `:root`, not `[data-theme="dark"]`. The dark semantic tokens reference them via `var()`, so the override propagates automatically when the dark theme is active. Light theme tokens do not reference these primitives, so overriding them has zero effect on light mode.
This is different from accent token overrides, which are set on `[data-theme]` selectors because they affect both themes.
### Changing spacing and radius
```css
:root {
--radius-sm: 0.125rem;
--radius-md: 0.25rem;
--radius-lg: 0.375rem;
--radius-full: 9999px;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-10: 2.5rem;
--space-12: 3rem;
--space-16: 4rem;
--space-24: 6rem;
--space-32: 8rem;
}
```
Spacing and radius primitives live on `:root` without a `--nuka-` prefix. Components reference them directly. Overriding them on `:root` affects all components globally.
### Nested themes
Because theming is anchored to a `data-theme` attribute, you can nest different themes on the same page. This works with any valid CSS selector scoping:
```html