Theming
Every color, radius, font and duration in WunderUI is a CSS variable. Change the variable and every component that uses it follows. No provider, no rebuild and no theme object.
How theming works
The stylesheet defines the tokens on for light and on for dark. Tailwind v4 turns them into utilities such as , or , and the components use only those utilities.
To theme, redefine the variables in your own CSS after the WunderUI import. Your values win because they come later. Set them on , and together: WunderUI defines each token for both modes, so a value on alone would be replaced in dark mode.
/* app/globals.css */
@import "tailwindcss";
@import "@wunderui/react/styles.css";
@source "../node_modules/@wunderui/react/dist";
:root,
.light,
.dark {
--primary: #0F9D76;
}Your brand color
The accent shows up in more places than the primary button: focus rings, links, the active sidebar item and the first chart series. Set these together so the whole interface changes color:
:root,
.light,
.dark {
--primary: #0F9D76; /* buttons, active states, selection */
--primary-foreground: #FFFFFF; /* text on primary */
--ring: #0F9D76; /* focus rings */
--sidebar-primary: #0F9D76; /* active sidebar item */
--chart-1: #0F9D76; /* first chart series */
}
/* Links sit on the page background: darker on white, lighter on dark. */
:root,
.light {
--text-link: #0B7A5C;
}
.dark {
--text-link: #5EE0BA;
}Check the contrast
Text on needs a contrast of at least 4.5:1 against . Links sit on the page background, so is often a darker shade of the accent in light mode and a lighter one in dark mode.
Or let the Theme Builder do it
The Theme Builder takes one colour and returns the whole brand scale ( … ), the light and dark tokens and a contrast check, as a to import after .
Color tokens
Components use semantic tokens, never raw colors. These are the ones you will change most often; the full list with light and dark values is on Color.
| Token | Used for |
|---|---|
| The page and its main text | |
| Surfaces: cards, menus, dialogs | |
| Quiet fills: hover backgrounds, tracks, input fills | |
| Dividers, outlines and field borders | |
| Supporting text and hints | |
| The accent, focus rings and links | |
| Errors and confirmations | |
| Status text that meets AA contrast | |
| Light badge and highlight fills in eight hues, with matching text | |
| Chart series, in order |
Tints are fixed per hue and do not follow . If your brand moves away from indigo, set and as well, or use the tint of the hue closest to your brand.
Dark mode
Dark values live on the class. Wrap your app in . It sets the class on , follows the system setting by default and remembers the reader's choice.
import { ThemeProvider } from "@wunderui/react"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}The provider switches without a transition, so nothing flickers while the colors change. A keyboard shortcut is off by default; pass (or ) to toggle light and dark from the keyboard while you build. Avoid a single letter in production: WCAG 2.1.4 then asks for a way to turn it off or remap it.
Theme one section
Variables inherit, so a theme can apply to part of a page. Set the variables on a wrapper and only the components inside it change:
<section style={{ "--primary": "#F47690", "--ring": "#F47690" } as React.CSSProperties}>
<Button>Uses the pink accent</Button>
</section>The same works for light and dark. A class on a wrapper shows that part in dark mode, and a class inside a dark page switches back, for a light card on a dark hero, for example.
Radius
| Token | Value | Used for |
|---|---|---|
| 4 px | Small chips and event markers | |
| 8 px | Buttons, inputs, menu and sidebar rows | |
| 12 px | Cards inside cards, attachment chips | |
| 16 px | Cards, dialogs, panels | |
| 24 px | Large feature surfaces |
Radius steps are Tailwind theme values, so change them in an block after the WunderUI import. Tailwind then builds and the other steps with your values:
@theme {
--radius-md: 6px; /* tighter buttons and inputs */
--radius-xl: 12px; /* flatter cards and dialogs */
}Unlike colors, radius cannot be changed for one section with a wrapper style. Keep the steps in order: a surface should have a larger radius than the controls inside it. The full radius and spacing scale, with the matching Figma variables, is on Spacing.
Fonts
Components read the font from . WunderUI is designed with Inter. With , pass the variable name and set it on :
import { Inter } from "next/font/google"
const inter = Inter({ subsets: ["latin"], variable: "--font-sans" })
<html className={`${inter.variable} font-sans`}>Any other font works the same way. Sizes and line heights are listed on Typography.
Motion
Durations ( to ) and easing curves (, , , ) are variables too. Slow the whole interface down, or make it snappier, by changing them in one place. Motion has a live editor that writes the CSS for you.
Users who ask for reduced motion get it without any work on your side: menus, dialogs, popovers and sheets appear and disappear at once instead of animating.
Keep Figma in sync
The token names are the same as the variables in the WunderUI Figma library. When you re-brand, change the variable in Figma and the CSS variable in code to the same value, and design and code stay in step.
The brand works the same way on both sides. Figma has a … scale in the Primitives collection, and , , and read from it, just like in code. Paste the scale from the Theme Builder into and the whole library re-brands, buttons, links, tints and the first chart series included.