Dark mode is one class

Redefine tokens under .dark on <html>; product UIs default to dark.

From shadcn (Vercel plugin): Dark Mode and product-surface defaults.

One class re-themes the tree

Toggle .dark; the left app hardcodes two colors.

Some colors hardcodedBad
<html class="">
Revenue
MRR $48,210
Churn 2.1% Last synced 2m ago
bg-white text-black text-black
Every color is a tokenGood
<html class="">
Revenue
MRR $48,210
Churn 2.1% Last synced 2m ago
bg-card text-card-foreground

/* globals.css */
:root {
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  --muted-foreground: oklch(0.556 0 0);
  --border: oklch(0.922 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);
  --muted-foreground: oklch(0.708 0 0);
  --border: oklch(1 0 0 / 10%);
}

<Card className="bg-card text-card-foreground">
<p className="text-card-foreground">Last synced 2m ago</p>

Default by product type

Pick a product type, then flip the mode.

Product type
Default:

Set the class before paint

Press Reload; drag the delay to stretch the flash.

Class set in useEffectBad
<html class="">
Deployments
-
Class in the HTMLGood
<html class="dark">
Deployments
-
// app/layout.tsx
<html lang="en" className="dark">

// with a toggle
import { ThemeProvider } from "next-themes";

<html lang="en" suppressHydrationWarning>
  <body>
    <ThemeProvider attribute="class" defaultTheme="dark" disableTransitionOnChange>
      {children}
    </ThemeProvider>
  </body>
</html>

Why it matters, pros and cons

Why it matters

Components name tokens, so one root class re-resolves every var().

Pros

  • No per-component dark: variants.
  • New components are themed automatically.
  • Hardcoded colors show up instantly.

Cons

  • Docs and blogs should default to light.
  • Toggles need a blocking script, or they flash.
  • Images and embeds need their own dark variants.