seedflip
Archive
Mixtapes
Pricing
Sign in

Building a Complete shadcn/ui Theme from Scratch (2026 Guide)

Every shadcn/ui project starts with the same gray theme. Zinc foreground, white background, the default look you've seen on a thousand starter templates. There's nothing wrong with it. But if you're building a product anyone will remember, you need a real theme. Not a color swap. A complete design system with semantic tokens, proper dark mode, chart colors, and intentional contrast ratios.

Generate a complete theme in seconds →

Start with your brand color

Every theme starts with one color. Your primary. This is the color of your call-to-action buttons, active states, and links. Everything else derives from this choice.

Pick one. For this guide, we'll use a warm indigo: 239 84% 67% in HSL. That's #6366F1 in hex.

Define your semantic token structure

shadcn/ui's theme system uses semantic tokens. Each variable has a specific role in the UI. Here's the full set you need to define:

/* The complete semantic token map */ --background /* Page background */ --foreground /* Default text color */ --card /* Card backgrounds */ --card-foreground /* Card text */ --popover /* Dropdowns, tooltips */ --popover-foreground /* Popover text */ --primary /* CTA buttons, links, active states */ --primary-foreground /* Text on primary backgrounds */ --secondary /* Secondary buttons, subtle actions */ --secondary-foreground/* Text on secondary backgrounds */ --muted /* Subtle backgrounds, disabled states */ --muted-foreground /* Secondary text, placeholders */ --accent /* Hover states, highlights */ --accent-foreground /* Text on accent backgrounds */ --destructive /* Delete buttons, error states */ --destructive-foreground --border /* All borders */ --input /* Input field borders */ --ring /* Focus ring color */ --radius /* Base border radius */ --chart-1 through --chart-5 /* Chart palette */

Build the light theme

Start with light mode. The relationships between tokens matter more than the specific values. Here's a complete light theme built from our indigo primary:

:root { /* Backgrounds */ --background: 0 0% 100%; --card: 0 0% 100%; --popover: 0 0% 100%; /* Text colors */ --foreground: 239 30% 10%; --card-foreground: 239 30% 10%; --popover-foreground: 239 30% 10%; /* Primary: your brand color */ --primary: 239 84% 67%; --primary-foreground: 0 0% 100%; /* Secondary: desaturated, subtle */ --secondary: 239 20% 95%; --secondary-foreground: 239 30% 15%; /* Muted: even more subtle */ --muted: 239 15% 95%; --muted-foreground: 239 10% 45%; /* Accent: related to primary, slightly different */ --accent: 239 20% 93%; --accent-foreground: 239 30% 15%; /* Destructive: red, always */ --destructive: 0 84% 60%; --destructive-foreground: 0 0% 98%; /* Chrome */ --border: 239 10% 90%; --input: 239 10% 90%; --ring: 239 84% 67%; --radius: 0.5rem; /* Charts */ --chart-1: 239 84% 67%; --chart-2: 173 58% 39%; --chart-3: 43 74% 66%; --chart-4: 27 87% 67%; --chart-5: 322 65% 55%; }

Notice the pattern. Secondary and muted use the same hue as primary (239) but with drastically reduced saturation and high lightness. This creates color cohesion without everything screaming indigo. Borders follow the same hue at very low saturation. Ring matches primary exactly so focus states are on-brand.

Build the dark theme

Dark mode is not inverted light mode. It's a separate palette that shares the same semantic structure. Key differences: backgrounds go dark, text goes light, accent colors get slightly more saturated to compensate for the dark surroundings, and borders need enough contrast to be visible.

.dark { /* Backgrounds: dark, slight warmth */ --background: 239 25% 5%; --card: 239 20% 8%; --popover: 239 20% 8%; /* Text: high contrast */ --foreground: 0 0% 95%; --card-foreground: 0 0% 95%; --popover-foreground: 0 0% 95%; /* Primary: slightly more saturated */ --primary: 239 90% 71%; --primary-foreground: 239 25% 5%; /* Secondary: subtle dark surface */ --secondary: 239 15% 14%; --secondary-foreground: 0 0% 90%; /* Muted */ --muted: 239 15% 14%; --muted-foreground: 239 10% 55%; /* Accent */ --accent: 239 15% 16%; --accent-foreground: 0 0% 90%; /* Destructive */ --destructive: 0 72% 51%; --destructive-foreground: 0 0% 98%; /* Chrome: borders need more contrast in dark */ --border: 239 10% 18%; --input: 239 10% 18%; --ring: 239 90% 71%; /* Charts: brighter for dark backgrounds */ --chart-1: 239 85% 72%; --chart-2: 173 65% 50%; --chart-3: 43 80% 70%; --chart-4: 27 90% 72%; --chart-5: 322 70% 62%; }

Add the Tailwind mapping

If you're on Tailwind v3, your tailwind.config.js maps these variables to utility classes. If you're on v4, use the @theme block:

/* Tailwind v4: @theme block in globals.css */ @theme { --color-background: hsl(var(--background)); --color-foreground: hsl(var(--foreground)); --color-primary: hsl(var(--primary)); --color-primary-foreground: hsl(var(--primary-foreground)); --color-secondary: hsl(var(--secondary)); --color-secondary-foreground: hsl(var(--secondary-foreground)); --color-muted: hsl(var(--muted)); --color-muted-foreground: hsl(var(--muted-foreground)); --color-accent: hsl(var(--accent)); --color-accent-foreground: hsl(var(--accent-foreground)); --color-destructive: hsl(var(--destructive)); --color-card: hsl(var(--card)); --color-card-foreground: hsl(var(--card-foreground)); --color-popover: hsl(var(--popover)); --color-popover-foreground: hsl(var(--popover-foreground)); --color-border: hsl(var(--border)); --color-input: hsl(var(--input)); --color-ring: hsl(var(--ring)); }

Test the theme systematically

Build a test page with every shadcn component to verify your theme. The minimum checklist:

1. Button (default, secondary, destructive, outline, ghost) in both light and dark.

2. Card with title, description, and content.

3. Input, Textarea, and Select with labels and placeholder text.

4. Dialog and Popover to verify overlay colors.

5. Table with alternating rows.

6. Alert (default and destructive).

7. Charts if you use the chart components.

The shortcut: design seeds

Building a theme from scratch takes time because the hard part isn't the CSS. It's the design decisions. Which hue for primary? How much saturation in the muted tokens? What border contrast ratio looks right in dark mode? These are taste decisions, and getting them wrong means hours of tweaking.

SeedFlip gives you 104 curated design seeds. Each one is a complete theme with colors, typography, spacing, shadows, and border radius. Every seed exports as a drop-in shadcn/ui theme block with all the variables above, both light and dark mode, chart colors included. The curation is the point. You get design decisions made by humans, not random generators.

/* SeedFlip export: complete shadcn/ui theme */ /* Just paste into globals.css */ :root { --background: 210 20% 98%; --foreground: 222 47% 11%; --primary: 262 83% 58%; --primary-foreground: 0 0% 100%; /* ... all 20+ variables */ --chart-1: 262 83% 58%; --chart-2: 173 58% 39%; --chart-3: 43 74% 66%; --chart-4: 27 87% 67%; --chart-5: 322 65% 55%; }

A real shadcn/ui theme needs roughly 25 CSS variables across light and dark modes, plus 5 chart colors. Get the semantic relationships right and everything from buttons to charts to popovers looks cohesive. For deeper customization of the globals.css structure, see How to Customize globals.css. For options beyond manual theming, check Best shadcn Theme Generators 2026. And for theming without touching Figma at all, read Theme shadcn Without Figma.

Ready to stop guessing?

One flip. Complete design system. Free CSS export.

Generate a complete theme in seconds →