Fix: shadcn/ui Not Working — Components Not Rendering, Styles Missing, or Dark Mode Broken
Part of: React & Frontend Errors
Quick Answer
How to fix shadcn/ui issues — Tailwind CSS v4 vs v3 configuration, CSS variables, dark mode setup, component installation, cn() utility, and common errors after adding components.
The Problem
After installing shadcn/ui components, they render with no styles:
npx shadcn@latest add button
# Component added but <Button> appears unstyledOr dark mode doesn’t work despite setting the class:
<html className="dark">
{/* Dark mode styles don't apply */}
</html>Or the cn() utility causes a type error:
import { cn } from '@/lib/utils';
// Error: Cannot find module '@/lib/utils'Or after upgrading Tailwind to v4, shadcn components break:
Error: Cannot apply unknown utility class: bg-backgroundWhy This Happens
shadcn/ui isn’t a component library you install as a package — it’s a CLI that copies component code into your project. Issues arise when the surrounding configuration isn’t set up correctly:
- CSS variables not injected — shadcn/ui uses CSS custom properties (
--background,--foreground, etc.) for theming. These must be added to your global CSS file. Without them, every component that usesbg-backgroundortext-foregroundrenders with no color. - Tailwind
contentpaths miss component files — Tailwind purges unused classes. If yourtailwind.configdoesn’t scan the right directories, classes used in shadcn components are removed from the build. - Dark mode requires
classstrategy — Tailwind’sdarkMode: 'class'applies dark styles when adarkclass is on the<html>element. The defaultmediastrategy uses the OS preference and ignores the class. - Tailwind v4 uses a different config format — shadcn/ui CLI was initially designed for Tailwind v3. Tailwind v4 uses CSS-based configuration (
@import "tailwindcss") instead oftailwind.config.js.
The CLI’s copy-paste distribution model is what most people misunderstand. When you run npx shadcn@latest add button, the CLI fetches a TypeScript file from the registry and writes it to components/ui/button.tsx inside your repo. There is no node_modules/shadcn-ui package to upgrade later. That means every customization you make stays yours, but every fix the upstream registry ships requires you to re-run the add command and merge the diff manually. If a component breaks after a Tailwind upgrade, the fix is almost always in the local file the CLI wrote — not in a dependency.
The other tripwire is the layered dependency chain. shadcn components sit on top of Radix UI primitives, which expose unstyled accessible behavior. Radix exposes props like data-state="open" that the shadcn styles target with selectors like data-[state=open]:bg-accent. If Tailwind isn’t scanning the file where those selectors are written, or if you accidentally remove the data-* attribute selectors when copying styles, the visual transitions silently stop. Always treat shadcn as three coordinated systems: the copied component file, the Tailwind utility pipeline, and the Radix primitive underneath.
Fix 1: Run the Init Command Correctly
# Start fresh — let the CLI configure everything
npx shadcn@latest init
# Answer the prompts:
# ✔ Which style would you like to use? › Default
# ✔ Which color would you like to use as the base color? › Slate
# ✔ Would you like to use CSS variables for theming? › yesThis command sets up:
components.json— shadcn configurationtailwind.config.ts— with the right content paths and pluginsglobals.css— with all CSS variableslib/utils.ts— with thecn()function
If you already have a project, verify these files exist:
ls components.json lib/utils.ts app/globals.css
# If any are missing, re-run: npx shadcn@latest initFix 2: Configure Tailwind Correctly
// tailwind.config.ts — required configuration for shadcn/ui
import type { Config } from 'tailwindcss';
const config: Config = {
// REQUIRED: dark mode must use 'class' strategy
darkMode: ['class'],
// REQUIRED: include all paths where Tailwind classes are used
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px',
},
},
extend: {
// REQUIRED: CSS variable-based color system
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
// Required for animations
keyframes: {
'accordion-down': {
from: { height: '0' },
to: { height: 'var(--radix-accordion-content-height)' },
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: '0' },
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
},
},
},
plugins: [require('tailwindcss-animate')],
};
export default config;Install required plugins:
npm install tailwindcss-animateFix 3: Add CSS Variables to globals.css
/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}Fix 4: Implement Dark Mode Toggling
// With next-themes (recommended for Next.js)
npm install next-themes
// app/providers.tsx
'use client';
import { ThemeProvider } from 'next-themes';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider
attribute="class" // Adds 'dark' class to <html>
defaultTheme="system" // Follow OS preference
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
);
}
// app/layout.tsx
import { Providers } from './providers';
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
{/* suppressHydrationWarning prevents hydration mismatch from theme */}
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
// components/theme-toggle.tsx
'use client';
import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';
import { Moon, Sun } from 'lucide-react';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
</Button>
);
}Fix 5: Use cn() and Customize Components
// lib/utils.ts — the cn() utility (created by shadcn init)
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// Install if missing:
// npm install clsx tailwind-mergeCustomize shadcn components:
// components/ui/button.tsx — add custom variants
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
// Base classes
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
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',
// Add custom variant
success: 'bg-green-600 text-white hover:bg-green-700',
},
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',
},
}
);
// Usage
<Button variant="success" size="lg">Save Changes</Button>Fix 6: Use with Tailwind v4
Tailwind v4 changed the configuration format. For projects using Tailwind v4:
# Check your Tailwind version
npx tailwindcss --version
# If v4, use the new CSS-based config approach/* globals.css — Tailwind v4 format */
@import "tailwindcss";
/* Tailwind v4 uses @theme instead of tailwind.config.js */
@theme {
--color-background: hsl(0 0% 100%);
--color-foreground: hsl(222.2 84% 4.9%);
--color-primary: hsl(222.2 47.4% 11.2%);
--color-primary-foreground: hsl(210 40% 98%);
--color-secondary: hsl(210 40% 96.1%);
--color-secondary-foreground: hsl(222.2 47.4% 11.2%);
--color-destructive: hsl(0 84.2% 60.2%);
--color-border: hsl(214.3 31.8% 91.4%);
--color-input: hsl(214.3 31.8% 91.4%);
--color-muted: hsl(210 40% 96.1%);
--color-muted-foreground: hsl(215.4 16.3% 46.9%);
--color-accent: hsl(210 40% 96.1%);
--color-popover: hsl(0 0% 100%);
--color-card: hsl(0 0% 100%);
--radius: 0.5rem;
}
/* Dark mode theme override */
.dark {
--color-background: hsl(222.2 84% 4.9%);
--color-foreground: hsl(210 40% 98%);
/* ... rest of dark vars */
}shadcn with Tailwind v4 — use the canary version:
# Use the shadcn canary that supports Tailwind v4
npx shadcn@canary init
# Add components with canary
npx shadcn@canary add buttonshadcn/ui vs Radix, Headless UI, Ark UI, React Aria — When Each Wins
shadcn/ui is one of five widely used approaches to building accessible React components, and the distribution model is the main difference. Choose based on how much code you want in your repo and how strict your accessibility requirements are.
shadcn/ui — copy-paste, you own the code. The CLI writes TypeScript files into your project. You can edit anything, but you also have to maintain anything. Tailwind classes are baked in, so the visual layer ships with the component. Best fit: teams that want full control over styling and a design system that lives inside their repo.
Radix UI — headless primitives, no styles. Radix gives you unstyled accessible behavior (focus management, ARIA, keyboard handling) as a regular npm package you upgrade like any dependency. You add styles yourself, usually with Tailwind or vanilla CSS. shadcn/ui is built on Radix — the difference is that with Radix alone you write the styling code from scratch. Best fit: teams with an existing design system that already owns visual decisions.
Headless UI — Tailwind Labs’ version of Radix. Similar headless API, designed to compose cleanly with Tailwind utility classes. Smaller surface area than Radix (Tabs, Listbox, Combobox, Dialog, Disclosure, Menu, Popover, RadioGroup, Switch, Transition), and the React and Vue implementations are kept in sync. Best fit: Tailwind-only projects that want Tailwind Labs to own the primitives.
Ark UI — multi-framework headless built on Zag.js state machines. Same headless idea as Radix, but the underlying state machines work across React, Vue, and Solid. Component coverage is broader than Headless UI (includes Combobox, DatePicker, Pagination, NumberInput, etc.) and stays consistent across frameworks. Best fit: monorepos that need the same components in multiple frameworks.
React Aria Components — Adobe’s accessibility-first primitives. The most thorough WAI-ARIA implementation of any library on this list, with international support (RTL, complex date pickers, virtualized collections) and the deepest documentation on screen reader behavior. The styling API uses render props and data-attributes similar to Radix. Best fit: enterprise apps with strict accessibility audits.
If you start with shadcn/ui and outgrow it (say, you need a feature not in the registry), you can usually drop down to Radix directly since shadcn components are thin wrappers. If you need richer primitives than Radix ships (calendar, complex combobox), Ark UI and React Aria become more appealing than maintaining your own. For pure Tailwind shops without strict a11y requirements, dropping shadcn for plain Radix plus Tailwind often resolves the issue before you switch libraries entirely.
Still Not Working?
Components render but look wrong after updating shadcn — shadcn components are copied into your project. When you run npx shadcn@latest add button on an existing project, it overwrites your local customizations. Check the diff before accepting the overwrite, and back up customized components before updating.
import { Button } from '@/components/ui/button' fails — the @/ alias must be configured in your TypeScript and bundler config:
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
}
}// vite.config.ts
import path from 'path';
export default defineConfig({
resolve: {
alias: { '@': path.resolve(__dirname, './src') },
},
});Radix UI peer dependency warning — shadcn components use Radix UI primitives. Version mismatches between Radix packages cause subtle bugs. Run npm ls @radix-ui to check versions. If there are conflicts, deduplicate: npm dedupe or reinstall with npm install --legacy-peer-deps. If a specific Radix primitive misbehaves on its own, see Radix UI not working for primitive-level fixes.
Dialog and Sheet content jumps or freezes on open — the most common cause is wrapping your app in a transformed parent (e.g., a CSS transform on <body> for page transitions). Radix portals into <body> by default, and a transformed ancestor breaks position: fixed for the overlay. Remove the transform on <body>, or pass container to the Radix Portal to escape the transformed subtree.
Theme switches flash unstyled content before hydration — the dark mode class is applied after React mounts, so the first paint uses the light theme. Fix this by reading the saved theme in a small inline script in <head> and applying the class before React renders. The next-themes package does this automatically when you set attribute="class" and add suppressHydrationWarning to <html>. If the flash persists, check that no other component is overriding the class on mount. See next-themes not working for theme provider edge cases.
Forms built with shadcn Form lose validation messages on submit — the Form component is a thin wrapper around react-hook-form. If validation errors appear during typing but vanish after submit, the form is being reset by a parent component re-render. Move state up so the form’s parent doesn’t unmount mid-submit, or pass shouldUnregister: false to the form. For broader patterns, see react-hook-form not working.
For related styling issues, see CSS Tailwind not applying.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: AutoAnimate Not Working — Transitions Not Playing, List Items Not Animating, or React State Changes Ignored
How to fix @formkit/auto-animate issues — parent ref setup, React useAutoAnimate hook, Vue directive, animation customization, disabling for specific elements, and framework integration.
Fix: Blurhash Not Working — Placeholder Not Rendering, Encoding Failing, or Colors Wrong
How to fix Blurhash image placeholder issues — encoding with Sharp, decoding in React, canvas rendering, Next.js image placeholders, CSS blur fallback, and performance optimization.
Fix: Embla Carousel Not Working — Slides Not Scrolling, Autoplay Not Starting, or Thumbnails Not Syncing
How to fix Embla Carousel issues — React setup, slide sizing, autoplay and navigation plugins, loop mode, thumbnail carousels, responsive breakpoints, and vertical scrolling.
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.