Next Starter Logo
Tutorials

Customizing the Theme

Update brand colors, switch fonts, configure dark mode, and add new Tailwind CSS design tokens using CSS custom properties.

The entire visual system is driven by CSS custom properties defined in app/globals.css. Tailwind CSS v4 maps these variables to utility classes, and shadcn/ui components reference the same variables. Changing a single CSS variable updates every button, card, input, and sidebar item at once.


Where CSS Variables Live

Open app/globals.css. The file is structured in three blocks:

  • @theme inline: maps CSS variable names to Tailwind's token system
  • :root: light mode values
  • .dark: dark mode values

You will only need to edit :root and .dark to change colors. The @theme inline block rarely needs touching unless you are adding entirely new design tokens.


Changing the Primary Color

The primary color controls buttons, links, active sidebar items, and focus rings. It is defined using the oklch color space, which makes it easier to create perceptually consistent color scales than hex or HSL.

app/globals.css
:root {
  /* Current: blue */
  --primary: oklch(0.5657 0.183562 254.571);
  --primary-hover: oklch(0.4727 0.14737 253.3486);
  --primary-foreground: oklch(0.985 0 0); /* text on primary backgrounds */
}

.dark {
  --primary: oklch(0.5657 0.183562 254.571);
  --primary-hover: oklch(0.4727 0.14737 253.3486);
  --primary-foreground: oklch(0.985 0 0);
}

Example: Switch to a Green Primary

app/globals.css
:root {
  --primary: oklch(0.55 0.18 145);
  --primary-hover: oklch(0.45 0.15 145);
  --primary-foreground: oklch(0.985 0 0);
}

.dark {
  --primary: oklch(0.60 0.18 145);
  --primary-hover: oklch(0.50 0.15 145);
  --primary-foreground: oklch(0.985 0 0);
}

The oklch format is oklch(lightness chroma hue):

  • Lightness: 0 (black) to 1 (white)
  • Chroma: 0 (gray) to ~0.37 (most saturated)
  • Hue: 0–360 degrees (red=25, orange=65, green=145, blue=254, purple=305)

A good tool for picking oklch values is oklch.com.

Keeping the Sidebar Accent Consistent

The active sidebar item uses --sidebar-accent, which defaults to the same blue as --primary. Update it when you change the primary color:

app/globals.css
:root {
  --sidebar-accent: oklch(0.55 0.18 145); /* match your new primary */
  --sidebar-accent-foreground: oklch(0.985 0 0);
}

Changing the Secondary Color

The secondary color is used for badges, secondary buttons, and accent highlights. In the starter it is an orange tone in light mode.

app/globals.css
:root {
  /* Current: orange */
  --secondary: oklch(0.6945 0.205 43.18);
  --secondary-foreground: oklch(0.985 0 0);
}

In dark mode, the secondary is intentionally toned down to a neutral so it does not compete with the dark background. Change both if you want a colorful secondary in both modes.


Changing Background and Surface Colors

For the page background and card surfaces:

app/globals.css
:root {
  --background: oklch(1 0 0);        /* page background (white) */
  --foreground: oklch(0.145 0 0);    /* default text (near-black) */
  --card: oklch(1 0 0);              /* card background */
  --card-foreground: oklch(0.145 0 0);
  --muted: oklch(0.98 0 0);          /* subtle background (e.g. input fills) */
  --muted-foreground: oklch(0.556 0 0); /* secondary text */
  --border: oklch(0.922 0 0);        /* borders and dividers */
}

.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);
  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);
  --border: oklch(1 0 0 / 10%);
}

Changing the Border Radius

The global border radius is set as a single variable and derived for smaller and larger variants:

app/globals.css
:root {
  --radius: 0.625rem; /* ~10px */
}

In @theme inline, the derived radii are:

--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);

To make the UI sharper, decrease --radius. For a pill-shaped look, increase it.


Changing Fonts

Fonts are loaded in app/layout.tsx using next/font/google. The starter uses Inter with variable: "--font-inter". The @theme inline block in globals.css maps this to --font-sans:

--font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif;

Switch to a Different Font

Replace Inter with any Google Font. In this example, switching to Geist. The variable name must match what @theme inline references:

app/layout.tsx
import { Geist } from "next/font/google";

const geist = Geist({
  subsets: ["latin"],
  display: "swap",
  variable: "--font-inter", // keeps @theme inline mapping working
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={geist.variable} suppressHydrationWarning>
      {/* ... */}
    </html>
  );
}

Using a Local Font

For a custom typeface, use next/font/local:

app/layout.tsx
import localFont from "next/font/local";

const myFont = localFont({
  src: "./fonts/MyFont.woff2",
  variable: "--font-inter",
  display: "swap",
});

Using a Separate Heading Font

The @theme inline block only maps --font-sans. To add a separate heading font, add a new variable and apply it in CSS:

app/globals.css
@theme inline {
  --font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
  --font-heading: var(--font-cal-sans), ui-sans-serif, system-ui, sans-serif;
}

Then load the heading font in layout.tsx with variable: "--font-cal-sans" and apply it selectively with font-[family-name:--font-heading] or a custom Tailwind class.


Dark Mode

Dark mode is handled by next-themes via the ThemeProvider in app/layout.tsx.

app/layout.tsx
<ThemeProvider
  attribute="class"      // adds/removes the "dark" class on <html>
  defaultTheme="system"  // follows OS preference by default
  enableSystem
  disableTransitionOnChange
>

When the dark class is present on <html>, Tailwind's dark: variant activates, and the .dark block in globals.css overrides the :root CSS variables. shadcn/ui components automatically respond to this because they read from the CSS variables.

The Theme Toggle

The starter includes a ThemeToggle component in components/theme-toggle.tsx. It uses the useTheme hook from next-themes to render a two-button toggle (light and dark). This component is wired into the dashboard sidebar's user dropdown menu, the authenticated user dropdown in the header, the mobile nav overlay, and the public footer.

Setting a Fixed Theme

To remove user-selectable theming, remove enableSystem and set a fixed defaultTheme:

<ThemeProvider attribute="class" defaultTheme="light">

How shadcn/ui Inherits the Theme

shadcn/ui components use the same CSS variables you defined. For example, the Button component uses bg-primary text-primary-foreground for its default variant. When you change --primary in globals.css, the button color updates without touching any component code.

The same applies to:

  • Card: uses --card and --card-foreground
  • Input: uses --input and --border
  • Badge: uses --secondary and --secondary-foreground
  • Dialog, Popover, DropdownMenu: use --popover and --popover-foreground

Inspect which variables each component uses by reading its source in components/ui/. The variable names match the Tailwind color token names (e.g. bg-card maps to --color-card which maps to var(--card)).


Adding New Design Tokens

To add a new token, for example an info color, add it in all three blocks:

app/globals.css
/* 1. Register it in the theme map */
@theme inline {
  --color-info: var(--info);
  --color-info-foreground: var(--info-foreground);
}

/* 2. Define light mode values */
:root {
  --info: oklch(0.55 0.18 230);
  --info-foreground: oklch(0.985 0 0);
}

/* 3. Define dark mode values */
.dark {
  --info: oklch(0.60 0.18 230);
  --info-foreground: oklch(0.985 0 0);
}

After this, use bg-info and text-info-foreground as Tailwind classes anywhere in the project.

On this page