Docs
Theming
Tokens
74 variables: surfaces, text levels, brand and status tones (each with a soft variant), elevation, stacking layers and motion. Tailwind maps them to utilities like bg-surface, text-muted-foreground, bg-success-soft and shadow-floating. Left light, right dark:
Surfaces
--backgroundoklch(1.0000 0 0) / oklch(0 0 0)--surface#FFFFFF / #1A1A1A--card#EBEBEB / oklch(0.1822 0 0)--popoveroklch(1.0000 0 0) / #262626--primary#000 / oklch(0.9850 0 0)
Text and lines
--foregroundoklch(0.3211 0 0) / oklch(0.9219 0 0)--muted-foregroundoklch(0.5200 0.0234 264.3637) / oklch(0.7155 0 0)--borderoklch(0.9276 0.0058 264.5313) / oklch(0.3715 0 0)--ringoklch(0.6000 0.2000 36.9340) / oklch(0.6617 0.2215 36.9340)
Tones
--brand#D63F00--success#15803D--warning#F5A524--destructive#DC2626--info#2563EB
Setup
npx wingo-ui@latest init merges the tokens into your global stylesheet, and add adds any a new component needs. Only missing names are added; values you already have stay. The full block is at /r/tokens.css (and in the MCP tool get_theme_tokens):
app/globals.css
:root {
--background: oklch(1.0000 0 0);
--foreground: oklch(0.3211 0 0);
--card: #EBEBEB;
--card-foreground: oklch(0.3211 0 0);
--popover: oklch(1.0000 0 0);
--popover-foreground: oklch(0.3211 0 0);
--surface: #FFFFFF;
--surface-foreground: oklch(0.3211 0 0);
--canvas: #F6F7F9;
--canvas-foreground: oklch(0.3211 0 0);
--primary: #000;
--primary-foreground: oklch(1.0000 0 0);
--secondary: oklch(0.9670 0.0029 264.5419);
--secondary-foreground: oklch(0.4461 0.0263 256.8018);
--muted: oklch(0.9846 0.0017 247.8389);
--muted-foreground: oklch(0.5200 0.0234 264.3637);
--accent: oklch(0.9640 0.0230 45.0000);
--accent-foreground: oklch(0.5200 0.1700 40.0000);
--destructive: #DC2626;
--destructive-foreground: oklch(1.0000 0 0);
--destructive-soft: #FDEDED;
--destructive-soft-foreground: #B42318;
--brand: #D63F00;
--brand-foreground: #FFFFFF;
--brand-soft: #FFEFE6;
--brand-soft-foreground: #B03400;
/* … */
}Dark mode
- Class (recommended): dark values in
.darkplus@custom-variant dark (&:is(.dark *));, toggled by next-themes withattribute="class". - System only: when your stylesheet already uses
@media (prefers-color-scheme: dark), the CLI puts the dark values there instead.
Theme generator
Pick a brand color, radius, font and component defaults in the theme generator, preview them on real components, then export theme.json and apply it:
npx wingo-ui@latest theme apply ./theme.jsonIt replaces the values of the tokens the theme sets (light and dark) and adds the Tailwind mappings for new ones. It prints the component defaults and locale for UIProvider.
Component defaults
UIProvider (item ui-config, installed with every component) sets app-wide defaults per component and the locale for dates and numbers. Explicit props always win.
// app/layout.tsx
import { ThemeProvider } from "next-themes"
import { UIProvider } from "@/lib/ui-config"
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
<UIProvider defaults={{ button: { radius: "full" }, input: { variant: "filled" } }} locale="en-US">
{children}
</UIProvider>
</ThemeProvider>Backgrounds and contrast
Never put a card, tile, panel, input, select or date picker on a background of the same or a very similar shade. Every nesting level steps the fill: the page, then bg-surface shadow-raised, then bg-foreground/5 tiles and soft fields or bg-background wells. A border-only design (one color, only borders separate the parts) is allowed but not recommended.
Pages alternate a main background with a secondary band. PageBackdrop sets both: background (plain, soft, glow, aurora, grid, dots), backgroundColor, bandBackground (muted, tinted, gradient, dots) and bandColor. The theme does not change them; pick a tinted pair that suits the brand rather than pure black.
import { MarketingSection, PageBackdrop } from "@/components/blocks/marketing-section"
<PageBackdrop background="aurora" backgroundColor="#0ea5e9" bandBackground="tinted">
<MarketingSection title="Features">{/* bg-surface cards */}</MarketingSection>
<MarketingSection background="muted" title="Pricing">{/* a band: its cards are lifted */}</MarketingSection>
</PageBackdrop>
// or app-wide
<UIProvider defaults={{ "page-backdrop": { background: "aurora", backgroundColor: "#0ea5e9" } }}>Rules of thumb
- Semantic tokens only: no hex values or arbitrary colors in app code.
- Neutral by default; the brand tone for the one main action on a screen.
- Colored text uses the soft foreground tokens (
text-success-soft-foreground) for contrast in both themes. - No card, panel, tile or field on a fill of its own shade: step the fill at every level.
- Check every change in light and dark mode.
Related guides
Next.js and Tailwind CSS
Tailwind v4 Dark Mode: Class Toggle, Tokens, next-themes
Tailwind v4 dark mode with a class: the @custom-variant line, next-themes wiring, why dark: ignores your toggle, and tokens that keep both themes designed.
9 min read
Next.js and Tailwind CSS
Tailwind v4 Design Tokens: Colors, Dark Mode and Contrast
Tailwind v4 design tokens from a production React library: @theme inline, semantic surfaces, status colors, dark mode and a contrast check you can run in CI.
20 min read
Next.js and Tailwind CSS
shadcn Tailwind v4 Migration: Step by Step From v3
A shadcn tailwind v4 migration in order: the upgrade codemod, tailwind.config into @theme inline, hsl to oklch, border and ring fixes, and tw-animate-css.
10 min read