Dark Mode Theme System
ZeroDrag includes a professional dark mode using shadcn/ui's token-based theming.
What This System Does
The theme system provides:
- CSS variables - No hardcoded colors
- Automatic adaptation - All components respond to theme changes
- Persistent - Theme saved in localStorage
- System-aware - Respects user's OS preference
- Accessible - Proper contrast ratios
Why It Exists
Dark mode is expected in modern applications. This system provides a production-ready theming solution that works across all components automatically, with proper contrast and accessibility.
When You Need to Care About It
You'll interact with theming when:
- Customizing colors for your brand
- Adding new components that need theme support
- Creating custom color schemes
- Fixing theme-related styling issues
Architecture
Color Variables (`app/globals.css`)
All colors use OKLCH color space for perceptual uniformity:
:root {
--background: oklch(1 0 0); /* Light: White */
--foreground: oklch(0.145 0 0); /* Light: Near black */
--primary: oklch(0.205 0 0); /* Primary action */
}
.dark {
--background: oklch(0.145 0 0); /* Dark: Dark gray */
--foreground: oklch(0.985 0 0); /* Dark: Near white */
--primary: oklch(0.922 0 0); /* Primary inverted */
}Theme Provider (`ThemeProvider.tsx`)
Wraps your app in layout.tsx:
import { ThemeProvider } from "@/components/ThemeProvider";
<ThemeProvider>{children}</ThemeProvider>;Theme Toggle (`ThemeToggle.tsx`)
Add to any component:
import { ThemeToggle } from "@/components/ThemeToggle";
<ThemeToggle />;Color Tokens
Always use these semantic tokens instead of hardcoded colors:
| Token | Usage | Example |
|---|---|---|
| background | Page background | bg-background |
| foreground | Primary text | text-foreground |
| card | Card background | bg-card |
| muted | Muted backgrounds | bg-muted |
| primary | Primary buttons | bg-primary |
| border | Borders | border-border |
Usage
✅ Correct Usage
// Good - uses theme tokens
<div className="bg-background text-foreground border-border">
<p className="text-muted-foreground">Muted text</p>
</div>
<Card>
<CardTitle>Automatically themed</CardTitle>
</Card>❌ Avoid
// Bad - hardcoded colors won't adapt
<div className="bg-white text-gray-900">
<p className="text-gray-500">Text</p>
</div>Migration Guide
Replace hardcoded colors with semantic tokens:
bg-white→bg-backgroundorbg-cardtext-gray-900→text-foregroundtext-gray-500→text-muted-foregroundborder-gray-200→border-borderbg-gray-100→bg-muted
Customization
Change Colors
Edit app/globals.css:
:root {
--primary: oklch(0.5 0.15 240); /* Blue */
}
.dark {
--primary: oklch(0.7 0.15 240); /* Lighter blue */
}Tools:
Add Custom Colors
1. Add to both `:root` and `.dark`:
:root {
--my-color: oklch(0.7 0.2 50);
}
.dark {
--my-color: oklch(0.6 0.2 50);
}2. Register in `@theme inline`:
@theme inline {
--color-my-color: var(--my-color);
}3. Use in components:
<div className="bg-my-color">Content</div>Advanced Features
Force Theme Programmatically
import { useTheme } from "next-themes";
const { setTheme } = useTheme();
setTheme("dark"); // or "light"Per-Section Themes
<div className="dark">
<Card>Always dark</Card>
</div>Add More Themes
1. Add CSS class:
.midnight {
--background: oklch(0.05 0 0);
--foreground: oklch(0.95 0 0);
}2. Update ThemeProvider:
themes={["light", "dark", "midnight"]}Troubleshooting
Hydration Mismatch
Ensure suppressHydrationWarning on <html>:
<html lang="en" suppressHydrationWarning>Colors Not Changing
- Check for hardcoded colors (
bg-whitevsbg-background) - Verify
.darkclass on<html>element - Debug with:typescript
const { theme, resolvedTheme } = useTheme(); console.log({ theme, resolvedTheme });
File Structure
Core Files (🔒 DO NOT MODIFY):
app/globals.css- Color variablescomponents/ThemeProvider.tsx- Theme contextcomponents/ThemeToggle.tsx- Toggle buttonapp/layout.tsx- Wraps app
Editable Files (🏗️ SAFE TO EDIT):
components/Header.tsx- UI placementcomponents/landing/LandingNav.tsx- UI placement
Related Sections
- UI Components - Component system that uses theme tokens