shadcn/ui already uses CSS variables with semantic names (shadcn/ui theming docs). You don't need to add a color system — you need to map the 60-30-10 split to the variables shadcn already reads.
Do it right and every <Button>, <Card>, and <Badge> inherits your palette with no additional per-component work.
Generate your shadcn palette at sixtythirtyten.co →
The direct mapping
The 60-30-10 rule assigns each color a role: dominant (largest surface area), secondary (structural elements), accent (calls to action). shadcn uses its own semantic variable names, but the roles map directly:
| 60-30-10 role | shadcn variable | Where it appears |
|---------------|-----------------|-----------------|
| Dominant (60%) | --background | Page background, content area |
| Secondary (30%) | --card, --secondary | Cards, sidebars, secondary surfaces |
| Accent (10%) | --primary | Buttons, links, active states |
One thing to know before you look at the code: shadcn uses HSL format without the hsl() wrapper. The value 210 40% 98% means hue 210, saturation 40%, lightness 98%. The wrapper is stripped because Tailwind handles it — this lets the framework add opacity modifiers like bg-primary/80 without breaking the CSS.
The :root block in globals.css
This is the full override. Drop it into your app/globals.css (or wherever shadcn added its CSS variable block) and replace the values with your palette:
:root {
--background: 210 40% 98%; /* dominant — 60% */
--foreground: 222 47% 11%;
--card: 210 40% 98%; /* secondary surface */
--card-foreground: 222 47% 11%;
--secondary: 217 33% 17%; /* secondary — 30% */
--secondary-foreground: 210 40% 98%;
--primary: 221 83% 53%; /* accent — 10% */
--primary-foreground: 0 0% 100%;
--accent: 210 40% 96%;
--accent-foreground: 222 47% 11%;
--muted: 210 40% 96%;
--muted-foreground: 215 16% 47%;
--border: 214 32% 91%;
--input: 214 32% 91%;
--ring: 221 83% 53%;
}
The palette above uses:
- Dominant:
#f5f9ff(light cool blue-white) for--background - Secondary:
#1e2d42(dark navy) for--secondary - Accent:
#3b82f6(blue) for--primary
Change those three and the rest of the values adjust proportionally — shadcn uses the foreground, muted, and border values to maintain readable contrast.
Dark mode: the .dark selector
shadcn uses a .dark class selector for dark mode, not prefers-color-scheme. This means dark mode is toggled by adding the dark class to the <html> element — giving users (and your app) explicit control over the theme. Manage the toggle with next-themes in Next.js apps.
Add the .dark block immediately after :root:
.dark {
--background: 222 47% 11%; /* dominant — dark surface */
--foreground: 210 40% 98%;
--card: 217 33% 17%;
--card-foreground: 210 40% 98%;
--secondary: 217 33% 27%; /* slightly lighter in dark */
--secondary-foreground: 210 40% 98%;
--primary: 217 91% 60%; /* accent — lighter blue for dark bg */
--primary-foreground: 222 47% 11%;
--accent: 217 33% 17%;
--accent-foreground: 210 40% 98%;
--muted: 217 33% 17%;
--muted-foreground: 215 20% 65%;
--border: 217 33% 17%;
--input: 217 33% 17%;
--ring: 224 76% 48%;
}
In dark mode the dominant color flips to your darkest surface. The accent color should lighten so it maintains contrast against the dark background — #60a5fa (light blue) reads clearly on near-black.
Hex-to-HSL conversion
The generator at sixtythirtyten.co outputs hex values. To use them with shadcn, convert to HSL components — without the hsl() wrapper.
Example: #3b82f6 (blue) converts to hsl(217, 91%, 60%). In shadcn format, that's 217 91% 60%.
To convert:
- Use a CSS tool or browser DevTools color picker (click any color swatch and switch to HSL mode)
- Drop the
hsl()wrapper — use only the three numbers:H S% L% - Paste into your
globals.css
For the dominant and secondary values, the same rule applies: #f8fafc → 210 40% 98%, #1e293b → 222 47% 11%.
Why this works
Every shadcn component reads from these CSS variables at render time. Change --primary from 221 83% 53% to 270 70% 50% (violet) and every <Button variant="default">, every active <NavigationMenuItem>, and every focus ring switches to violet — with no component code touched.
The three-variable approach follows the same principle as the 60-30-10 rule: centralize decisions so changes propagate automatically. One palette change, full UI update.
Complete globals.css pattern
For a Next.js app using shadcn/ui with full light/dark support:
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 210 40% 98%;
--foreground: 222 47% 11%;
--card: 210 40% 98%;
--card-foreground: 222 47% 11%;
--secondary: 217 33% 17%;
--secondary-foreground: 210 40% 98%;
--primary: 221 83% 53%;
--primary-foreground: 0 0% 100%;
--accent: 210 40% 96%;
--accent-foreground: 222 47% 11%;
--muted: 210 40% 96%;
--muted-foreground: 215 16% 47%;
--destructive: 0 84% 60%;
--destructive-foreground: 0 0% 98%;
--border: 214 32% 91%;
--input: 214 32% 91%;
--ring: 221 83% 53%;
--radius: 0.5rem;
}
.dark {
--background: 222 47% 11%;
--foreground: 210 40% 98%;
--card: 217 33% 17%;
--card-foreground: 210 40% 98%;
--secondary: 217 33% 27%;
--secondary-foreground: 210 40% 98%;
--primary: 217 91% 60%;
--primary-foreground: 222 47% 11%;
--accent: 217 33% 17%;
--accent-foreground: 210 40% 98%;
--muted: 217 33% 17%;
--muted-foreground: 215 20% 65%;
--destructive: 0 63% 31%;
--destructive-foreground: 0 0% 98%;
--border: 217 33% 17%;
--input: 217 33% 17%;
--ring: 224 76% 48%;
}
}
Related guides
- 60-30-10 Tailwind Color Palette: @theme and module.exports — if you're not using shadcn, see the standalone Tailwind config format
- CSS Variables Color System Cheat Sheet — framework-agnostic reference for the same pattern
- 60-30-10 Dark Mode Color Palette — contrast ratios, accent adjustment, and the difference between
.darkclass andprefers-color-scheme - SaaS App Color Token System — scaling beyond 3 variables with a full primitive → semantic → component token architecture
- The 60-30-10 Rule: A Developer's Guide to UI Color Balance — the foundational guide if you're new to the rule
Start with one color. The generator produces all three roles with the HSL values ready to paste into your globals.css.