Skip to content
General

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.

css
/* 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:

css
: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;
}

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.

TokenUsed 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.

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

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

TokenValueUsed for
4 pxSmall chips and event markers
8 pxButtons, inputs, menu and sidebar rows
12 pxCards inside cards, attachment chips
16 pxCards, dialogs, panels
24 pxLarge 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:

css
@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 :

tsx
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.