Complete Guide to shadcn/ui Installation and Theme Customization with CSS Variables
Honestly, when I first used shadcn/ui, I was confused by the “not an npm package” concept. Copy code into my project? That sounds too primitive, right?
After using it a few times, I realized this is actually what makes it powerful—you own all the component source code, modify it however you want, no version conflicts, no being locked into a component library’s design decisions.
Today let’s talk about shadcn/ui installation and theme customization, focusing on how to use CSS variables to build a branded design system. After reading this article, you should be able to set up the basics in 5 minutes, then spend an hour or so fine-tuning the theme to your liking.
1. Quick Installation: Two Approaches
Approach 1: CLI One-Liner (Recommended)
For new projects, just run this command:
npx shadcn@latest init
You’ll get asked a bunch of questions: TypeScript or JavaScript? Which style? Default theme? It’s all interactive, just follow the prompts.
After installation, your project will have these new files:
components.json- configuration filelib/utils.ts- utility functionscomponents/ui/- component directory
Adding components is simple too, like a button:
npx shadcn@latest add button
Component code gets copied to components/ui/button.tsx, just import and use it.
Here’s a catch: If your project has been in development for a while, tailwind.config.js and globals.css might have a lot of custom code. shadcn’s init command will overwrite these files, so it’s best to install at the very beginning.
One blogger put it well: treat shadcn/ui as your project’s “first dependency”, don’t wait until later to add it. Learned that the hard way.
Approach 2: Manual Installation (For Existing Projects)
If your project is already established and CLI overwriting config is too risky, install manually.
Here’s the breakdown:
Step 1: Make sure Tailwind CSS is installed
shadcn components are styled with Tailwind, so install it first if you haven’t. The official docs are pretty clear on this.
Step 2: Install dependencies
npm install class-variance-authority clsx tailwind-merge
npm install lucide-react
class-variance-authority (CVA for short) is useful—you’ll need it later for component variants.
Step 3: Configure path aliases
Add this to tsconfig.json:
{
"compilerOptions": {
"paths": {
"@/*": ["./*"]
}
}
}
Now you can import components like @/components/ui/button instead of writing ../../../ everywhere.
Step 4: Create components.json
Create this file in your project root:
{
"style": "new-york",
"rsc": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
That cssVariables: true line is key—it means we’re using CSS variables for theming, not Tailwind utility classes.
Step 5: Add styles
Add shadcn’s base styles to globals.css, we’ll cover this in detail when discussing themes.
2. Understanding the Theme System: How CSS Variables Work
shadcn/ui’s theme system is based on a simple convention: every color has both background and foreground variables.
What does that mean? Here’s an example:
:root {
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
}
--primary is the button’s background color, --primary-foreground is the text color on the button. The benefit of this pairing is that changing one variable updates all related components.
CSS Variables List
shadcn/ui defines these variables by default:
| Variable | Purpose |
|---|---|
--background | Page background |
--foreground | Page text |
--card | Card background |
--card-foreground | Card text |
--popover | Popup background |
--popover-foreground | Popup text |
--primary | Primary color (buttons, links) |
--primary-foreground | Text on primary color |
--secondary | Secondary color |
--secondary-foreground | Text on secondary color |
--muted | Muted background |
--muted-foreground | Muted text |
--accent | Accent color |
--accent-foreground | Text on accent color |
--destructive | Destructive actions (delete buttons) |
--destructive-foreground | Text on destructive color |
--border | Borders |
--input | Input fields |
--ring | Focus rings |
Looks like a lot, but once you understand the background/foreground pattern, it’s easy to remember.
The Secret of HSL Format
You might notice shadcn’s color values aren’t in standard HSL format:
/* ❌ Standard HSL */
--primary: hsl(222.2, 47.4%, 11.2%);
/* ✅ shadcn format */
--primary: 222.2 47.4% 11.2%;
Why write it in this “bare” format?
Because Tailwind supports opacity modifiers, like bg-primary/50 for 50% transparent primary color. If your variable is in full hsl() format, this feature won’t work.
With the bare format, Tailwind automatically adds hsl() and opacity for you. Smart design.
3. Customizing Your Brand Theme
Method 1: Modify CSS Variables Directly
The simplest approach—open globals.css, find the :root section, change the color values.
For example, changing primary from default blue to purple:
:root {
--primary: 270 60% 60%;
--primary-foreground: 0 0% 100%;
}
.dark {
--primary: 270 60% 70%;
--primary-foreground: 0 0% 0%;
}
Save it, and all components using bg-primary will turn purple.
Method 2: Use OKLCH Color Space (Tailwind v4)
If you’re using Tailwind v4, consider OKLCH color space. Compared to HSL, OKLCH color perception is closer to human vision, generating more uniform color scales.
:root {
--primary: oklch(0.6 0.2 270);
--primary-foreground: oklch(0.98 0 0);
}
The three parameters of oklch(0.6 0.2 270) are:
0.6- lightness (0-1)0.2- chroma (around 0-0.4)270- hue angle (0-360)
Method 3: Use Online Tools
Think configuring colors is too much hassle? Use online tools.
I recommend this one: Shadcn Theme Generator
Pick a primary color, the tool automatically generates a complete CSS variable set, including both light and dark versions. Copy-paste into globals.css and you’re done.
4. Dark Mode Configuration
Using next-themes for Theme Switching
shadcn/ui doesn’t include theme switching out of the box, but you can use the next-themes library.
Install it first:
npm install next-themes
Then configure in layout.tsx:
import { ThemeProvider } from "next-themes"
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
>
{children}
</ThemeProvider>
</body>
</html>
)
}
Key points:
suppressHydrationWarningis required, otherwise you’ll get hydration warningsattribute="class"means using class names to switch themesdefaultTheme="system"means follow system preference by defaultenableSystemenables system theme detection
Creating a Theme Toggle Button
Use the useTheme hook to get current theme and switch function:
import { useTheme } from "next-themes"
import { Moon, Sun } from "lucide-react"
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
return (
<button
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
className="p-2 rounded-md hover:bg-accent"
>
{theme === "dark" ? <Sun size={20} /> : <Moon size={20} />}
</button>
)
}
Default Dark Mode
If you want your site to default to dark mode, there are two ways:
Way 1: Hardcode dark class
<html lang="en" className="dark">
This locks the theme to dark, no switching.
Way 2: Set default theme
<ThemeProvider
attribute="class"
defaultTheme="dark" // Default dark
enableSystem={false} // Disable system detection
>
Users can still manually switch, but initial state is dark.
5. Advanced Customization: Component Variants
Creating Custom Variants with CVA
Sometimes you need to add multiple styles to buttons, like “danger”, “success”, “gradient”. CVA makes defining these variants easy.
import { cva, type VariantProps } from "class-variance-authority"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
Then use it in components:
<button className={buttonVariants({ variant: "destructive", size: "lg" })}>
Delete
</button>
Don’t Modify shadcn Component Source Code Directly
This is a best practice issue.
shadcn’s component code is in your own project, you can modify it however you want. But I recommend not modifying the original files—create wrapper components instead.
Why? shadcn frequently updates components. If you modify the original files, you’ll have to manually merge when updating. That’s a pain.
Better approach:
// components/brand-button.tsx
import { Button } from "@/components/ui/button"
import { cva } from "class-variance-authority"
const brandButtonVariants = cva("...", {
variants: {
brand: {
primary: "bg-brand-primary text-white",
secondary: "bg-brand-secondary text-black",
},
},
})
export function BrandButton({ brand, ...props }) {
return <Button className={brandButtonVariants({ brand })} {...props} />
}
This way the original Button component stays unchanged, you’ve created your own BrandButton, and future shadcn updates won’t affect your customizations.
6. Common Issues and Gotchas
Issue 1: Styles Not Working After Installation
Check these things:
- Is
globals.cssimported inlayout.tsx? - Does Tailwind’s
contentconfig includecomponents/**/*? - Are the paths in
components.jsoncorrect?
Issue 2: Theme Switching Flicker
This is usually caused by hydration mismatch. Make sure:
<html>tag hassuppressHydrationWarning- ThemeProvider wraps the entire app
- Don’t read theme during server-side rendering (will be undefined)
Issue 3: CSS Variables Not Working
Possible causes:
- Typo in variable name (it’s
--primary-foregroundnot--primaryForeground) - Missing corresponding
.darkstyles - Wrong variable value format (use bare HSL or OKLCH)
Issue 4: Component Style Conflicts
If your project already has a style system, it might conflict with shadcn’s. Solutions:
- Add namespace to shadcn components (like
shadcn-button) - Adjust Tailwind layer priorities
- Create your own variants with CVA, don’t rely on default styles
7. Summary
shadcn/ui installation and configuration is actually pretty simple—the key is understanding its “copy code not dependency package” design philosophy. The benefit is complete control, the downside is maintaining your own component code in every project.
For theme customization, the CSS variable system is elegantly designed—change a few variable values and the entire app’s colors update. With next-themes, light/dark mode switching is just a few lines of code.
Final recommendations:
- Use CLI initialization for new projects—skip the manual configuration hassle
- Use semantic color variables like primary, secondary, not specific color names
- Test contrast in both light and dark modes—ensure readability
- Create wrapper components instead of modifying source—makes updates easier
Next time you need to quickly build a themed UI, give shadcn/ui a try. The joy of copy-paste, you’ll understand once you use it.
shadcn/ui Installation and Theme Customization
Install shadcn/ui from scratch, configure theme system, build branded design
⏱️ Estimated time: 30 min
- 1
Step1: CLI Quick Initialization
Run the installation command in a new project:
• npx shadcn@latest init
• Choose TypeScript/New York style/default theme
• Wait for CLI to finish configuration - 2
Step2: Modify Brand Primary Color
Edit CSS variables in globals.css:
• Open app/globals.css
• Find --primary variable under :root
• Change to your brand color (HSL or OKLCH format)
• Also modify --primary-foreground for contrast - 3
Step3: Configure Dark Mode
Install and configure next-themes:
• npm install next-themes
• Add ThemeProvider in layout.tsx
• Set suppressHydrationWarning to avoid hydration warning
• Create theme toggle component - 4
Step4: Create Component Variants
Use CVA to define custom styles:
• Install class-variance-authority
• Define variants and defaultVariants
• Apply buttonVariants() in components
• Keep original shadcn components unchanged
FAQ
What's the difference between shadcn/ui and traditional UI component libraries?
Why install shadcn/ui when initializing a new project?
Why use bare format for CSS variables instead of standard HSL?
How do I change the brand primary color?
Why does dark mode flicker?
Should I modify shadcn component source code directly?
References
- shadcn/ui Official Docs - Installation
- shadcn/ui Official Docs - Theming
- shadcn/ui Official Docs - Dark Mode
- Generate Custom shadcn/ui Themes
- Theming in shadcn UI: CSS Variables
8 min read · Published on: Mar 26, 2026 · Modified on: Mar 26, 2026
Related Posts
What is shadcn/ui? How to Choose Between MUI, Chakra, and Ant Design
What is shadcn/ui? How to Choose Between MUI, Chakra, and Ant Design
Tailwind CSS v4 Features Deep Dive: Performance, Configuration, and Migration Guide
Tailwind CSS v4 Features Deep Dive: Performance, Configuration, and Migration Guide
Tailwind v4 + Vite: Complete Setup Template in 5 Minutes

Comments
Sign in with GitHub to leave a comment