Theming

Using CSS variables and theme tokens.

We use and recommend CSS variables for theming.

This gives you semantic theme tokens like background, foreground, and primary that components use by default. Override those tokens in your CSS to change the look of your app without rewriting component classes.

<div className="bg-background text-foreground" />

To use CSS variables for theming, set tailwind.cssVariables to true in your components.json file. This is the default.

components.json
{
  "style": "base-nova",
  "rsc": true,
  "tailwind": {
    "config": "",
    "css": "app/globals.css",
    "baseColor": "neutral",
    "cssVariables": true
  }
}

Tailwind maps these tokens into utilities like bg-background, text-foreground, border-border, and ring-ring.

Dark mode works by overriding the same tokens inside a .dark selector. See the dark mode docs for adding a theme provider and toggling the .dark class.

Token Convention

We use semantic background and foreground pairs. The base token controls the surface color and the -foreground token controls the text and icon color that sits on that surface.

Given the following CSS variables:

--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);

The background color of the following component will be var(--primary) and the foreground color will be var(--primary-foreground).

<div className="bg-primary text-primary-foreground">Hello</div>

Theme Tokens

These tokens live in your CSS file under :root and .dark.

TokenWhat it controlsUsed by
background / foregroundThe default app background and text color.The page shell, page sections, and default text.
card / card-foregroundElevated surfaces and the content inside them.Card, dashboard panels, settings panels.
popover / popover-foregroundFloating surfaces and the content inside them.Popover, DropdownMenu, ContextMenu, and other overlays.
primary / primary-foregroundHigh-emphasis actions and brand surfaces.Default Button, selected states, badges, and active accents.
secondary / secondary-foregroundLower-emphasis filled actions and supporting surfaces.Secondary buttons, secondary badges, and supporting UI.
muted / muted-foregroundSubtle surfaces and lower-emphasis content.Descriptions, placeholders, empty states, helper text, and subdued surfaces.
accent / accent-foregroundInteractive hover, focus, and active surfaces.Ghost buttons, menu highlight states, hovered rows, and selected items.
destructiveDestructive actions and error emphasis.Destructive buttons, invalid states, and destructive menu items.
borderDefault borders and separators.Cards, menus, tables, separators, and layout dividers.
inputForm control borders and input surface treatment.Input, Textarea, Select, and outline-style controls.
ringFocus rings and outlines.Buttons, inputs, checkboxes, menus, and other focusable controls.
chart-1 ... chart-5The default chart palette.Charts and chart-driven dashboard blocks.
sidebar / sidebar-foregroundThe base sidebar surface and default sidebar text.The Sidebar container and its default content.
sidebar-primary / sidebar-primary-foregroundHigh-emphasis actions inside the sidebar.Active items, icon tiles, badges, and sidebar CTAs.
sidebar-accent / sidebar-accent-foregroundHover and selected states inside the sidebar.Sidebar menu hover states, open items, and interactive rows.
sidebar-borderSidebar-specific borders and separators.Sidebar headers, groups, and internal dividers.
sidebar-ringSidebar-specific focus rings.Focused controls inside the sidebar.
radiusThe base corner radius scale.Cards, inputs, buttons, popovers, and the derived radius-* tokens.

Radius Scale

--radius is the base radius token for your theme.

We derive a small radius scale from it so components can use consistent corner sizes while still sharing a single source of truth.

app/globals.css
@theme inline {
  --radius-sm: calc(var(--radius) * 0.6);
  --radius-md: calc(var(--radius) * 0.8);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) * 1.4);
  --radius-2xl: calc(var(--radius) * 1.8);
  --radius-3xl: calc(var(--radius) * 2.2);
  --radius-4xl: calc(var(--radius) * 2.6);
}

This means:

  • radius-lg is the base value.
  • Smaller radii scale down from --radius.
  • Larger radii scale up from --radius.
  • Changing --radius updates the entire radius scale.

Adding New Tokens

To add a new token, define it under :root and .dark, then expose it to Tailwind with @theme inline.

app/globals.css
:root {
  --warning: oklch(0.84 0.16 84);
  --warning-foreground: oklch(0.28 0.07 46);
}
 
.dark {
  --warning: oklch(0.41 0.11 46);
  --warning-foreground: oklch(0.99 0.02 95);
}
 
@theme inline {
  --color-warning: var(--warning);
  --color-warning-foreground: var(--warning-foreground);
}

You can now use bg-warning and text-warning-foreground in your components.

<div className="bg-warning text-warning-foreground" />

Base Colors

tailwind.baseColor controls the default token values generated for your project when you run init or use a preset.

The available base colors are: Neutral, Stone, Zinc, Mauve, Olive, Mist, and Taupe.

Default Theme CSS

The following is the full default neutral theme scaffold. Copy it into your global CSS file and adjust the tokens as needed.

app/globals.css
@import "tailwindcss";
@import "shadcn/tailwind.css";
 
@custom-variant dark (&:is(.dark *));
 
@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-card: var(--card);
  --color-card-foreground: var(--card-foreground);
  --color-popover: var(--popover);
  --color-popover-foreground: var(--popover-foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  --color-secondary: var(--secondary);
  --color-secondary-foreground: var(--secondary-foreground);
  --color-muted: var(--muted);
  --color-muted-foreground: var(--muted-foreground);
  --color-accent: var(--accent);
  --color-accent-foreground: var(--accent-foreground);
  --color-destructive: var(--destructive);
  --color-border: var(--border);
  --color-input: var(--input);
  --color-ring: var(--ring);
  --color-chart-1: var(--chart-1);
  --color-chart-2: var(--chart-2);
  --color-chart-3: var(--chart-3);
  --color-chart-4: var(--chart-4);
  --color-chart-5: var(--chart-5);
  --color-sidebar: var(--sidebar);
  --color-sidebar-foreground: var(--sidebar-foreground);
  --color-sidebar-primary: var(--sidebar-primary);
  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
  --color-sidebar-accent: var(--sidebar-accent);
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
  --color-sidebar-border: var(--sidebar-border);
  --color-sidebar-ring: var(--sidebar-ring);
  --radius-sm: calc(var(--radius) * 0.6);
  --radius-md: calc(var(--radius) * 0.8);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) * 1.4);
  --radius-2xl: calc(var(--radius) * 1.8);
  --radius-3xl: calc(var(--radius) * 2.2);
  --radius-4xl: calc(var(--radius) * 2.6);
}
 
:root {
  --radius: 0.625rem;
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  --popover: oklch(1 0 0);
  --popover-foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);
  --accent: oklch(0.97 0 0);
  --accent-foreground: oklch(0.205 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);
  --chart-1: oklch(0.646 0.222 41.116);
  --chart-2: oklch(0.6 0.118 184.704);
  --chart-3: oklch(0.398 0.07 227.392);
  --chart-4: oklch(0.828 0.189 84.429);
  --chart-5: oklch(0.769 0.188 70.08);
  --sidebar: oklch(0.985 0 0);
  --sidebar-foreground: oklch(0.145 0 0);
  --sidebar-primary: oklch(0.205 0 0);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.97 0 0);
  --sidebar-accent-foreground: oklch(0.205 0 0);
  --sidebar-border: oklch(0.922 0 0);
  --sidebar-ring: oklch(0.708 0 0);
}
 
.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.205 0 0);
  --card-foreground: oklch(0.985 0 0);
  --popover: oklch(0.205 0 0);
  --popover-foreground: oklch(0.985 0 0);
  --primary: oklch(0.922 0 0);
  --primary-foreground: oklch(0.205 0 0);
  --secondary: oklch(0.269 0 0);
  --secondary-foreground: oklch(0.985 0 0);
  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);
  --accent: oklch(0.269 0 0);
  --accent-foreground: oklch(0.985 0 0);
  --destructive: oklch(0.704 0.191 22.216);
  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 15%);
  --ring: oklch(0.556 0 0);
  --chart-1: oklch(0.488 0.243 264.376);
  --chart-2: oklch(0.696 0.17 162.48);
  --chart-3: oklch(0.769 0.188 70.08);
  --chart-4: oklch(0.627 0.265 303.9);
  --chart-5: oklch(0.645 0.246 16.439);
  --sidebar: oklch(0.205 0 0);
  --sidebar-foreground: oklch(0.985 0 0);
  --sidebar-primary: oklch(0.488 0.243 264.376);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.269 0 0);
  --sidebar-accent-foreground: oklch(0.985 0 0);
  --sidebar-border: oklch(1 0 0 / 10%);
  --sidebar-ring: oklch(0.556 0 0);
}
 
@layer base {
  * {
    @apply border-border outline-ring/50;
  }
 
  body {
    @apply bg-background text-foreground;
  }
}

Without CSS Variables

If you do not want to use CSS variables, the CLI can generate components with inline Tailwind color utilities instead.

pnpm dlx shadcn@latest init --no-css-variables

This sets tailwind.cssVariables to false in your components.json file.

<div className="bg-zinc-950 text-zinc-50 dark:bg-white dark:text-zinc-950" />