Long-form reading stays on paper-like light.
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="">
MRR
$48,210
Churn
2.1%
Last synced 2m ago
bg-white text-black text-blackEvery color is a tokenGood
<html class="">
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.
Default:
Set the class before paint
Press Reload; drag the delay to stretch the flash.
Class set in useEffectBad
<html class="">-Class in the HTMLGood
<html class="dark">-// 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.