tailwind-v4-shadcn

Tailwind v4 with shadcn/ui using CSS variables and @theme inline pattern. Four-step architecture: define CSS variables at root, map to Tailwind utilities with @theme inline , apply base styles, automatic dark mode switching Prevents 8 documented errors including color mapping failures, dark mode conflicts, @apply breaking changes, and v3 migration gotchas Requires @tailwindcss/vite plugin (not PostCSS), empty Tailwind config in components.json, and ThemeProvider wrapper for theme toggling Supports OKLCH color space (v4 default), built-in container queries and line clamp, and multi-theme setups with proper CSS layer ordering

INSTALLATION
npx skills add https://github.com/jezweb/claude-skills --skill tailwind-v4-shadcn
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$2a

2. Delete v3 config if exists

rm tailwind.config.ts # v4 doesn't use this file

**vite.config.ts**:

import { defineConfig } from 'vite'

import react from '@vitejs/plugin-react'

import tailwindcss from '@tailwindcss/vite'

import path from 'path'

export default defineConfig({

plugins: [react(), tailwindcss()],

resolve: { alias: { '@': path.resolve(__dirname, './src') } }

})


**components.json** (CRITICAL):

{

"tailwind": {

"config": "", // ← Empty for v4

"css": "src/index.css",

"baseColor": "slate",

"cssVariables": true

}

}


## The Four-Step Architecture (MANDATORY)

Skipping steps will break your theme. Follow exactly:

### Step 1: Define CSS Variables at Root

/ src/index.css /

@import "tailwindcss";

@import "tw-animate-css"; / Required for shadcn/ui animations /

:root {

--background: hsl(0 0% 100%); / ← hsl() wrapper required /

--foreground: hsl(222.2 84% 4.9%);

--primary: hsl(221.2 83.2% 53.3%);

/ ... all light mode colors /

}

.dark {

--background: hsl(222.2 84% 4.9%);

--foreground: hsl(210 40% 98%);

--primary: hsl(217.2 91.2% 59.8%);

/ ... all dark mode colors /

}


**Critical**: Define at root level (NOT inside `@layer base`). Use `hsl()` wrapper.

### Step 2: Map Variables to Tailwind Utilities

@theme inline {

--color-background: var(--background);

--color-foreground: var(--foreground);

--color-primary: var(--primary);

/ ... map ALL CSS variables /

}


**Why**: Generates utility classes (`bg-background`, `text-primary`). Without this, utilities won't exist.

### Step 3: Apply Base Styles

@layer base {

body {

background-color: var(--background); / NO hsl() wrapper here /

color: var(--foreground);

}

}


**Critical**: Reference variables directly. Never double-wrap: `hsl(var(--background))`.

### Step 4: Result - Automatic Dark Mode

<div className="bg-background text-foreground">

{/ No dark: variants needed - theme switches automatically /}

</div>


## Dark Mode Setup

**1. Create ThemeProvider** (see `templates/theme-provider.tsx`)

**2. Wrap App**:

// src/main.tsx

import { ThemeProvider } from '@/components/theme-provider'

ReactDOM.createRoot(document.getElementById('root')!).render(

<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">

<App />

</ThemeProvider>

)


**3. Add Theme Toggle**:

pnpm dlx shadcn@latest add dropdown-menu


See `reference/dark-mode.md` for ModeToggle component.

## Critical Rules

### ✅ Always Do:

- Wrap colors with `hsl()` in `:root`/`.dark`: `--bg: hsl(0 0% 100%);`

- Use `@theme inline` to map all CSS variables

- Set `"tailwind.config": ""` in components.json

- Delete `tailwind.config.ts` if exists

- Use `@tailwindcss/vite` plugin (NOT PostCSS)

### ❌ Never Do:

- Put `:root`/`.dark` inside `@layer base` (causes cascade issues)

- Use `.dark { @theme { } }` pattern (v4 doesn't support nested @theme)

- Double-wrap colors: `hsl(var(--background))`

- Use `tailwind.config.ts` for theme (v4 ignores it)

- Use `@apply` directive (deprecated in v4, see error #7)

- Use `dark:` variants for semantic colors (auto-handled)

- Use `@apply` with `@layer base` or `@layer components` classes (v4 breaking change - use `@utility` instead) | [Source](https://github.com/tailwindlabs/tailwindcss/discussions/17082)

- Wrap ANY styles in `@layer base` without understanding CSS layer ordering (see error #8) | [Source](https://github.com/tailwindlabs/tailwindcss/discussions/16002)

## Common Errors &#x26; Solutions

This skill prevents **8 documented errors**.

### 1. ❌ tw-animate-css Import Error

**Error**: "Cannot find module 'tailwindcss-animate'"

**Cause**: shadcn/ui deprecated `tailwindcss-animate` for v4.

**Solution**:

✅ DO

pnpm add -D tw-animate-css

Add to src/index.css:

@import "tailwindcss";

@import "tw-animate-css";

❌ DON'T

npm install tailwindcss-animate # v3 only


### 2. ❌ Colors Not Working

**Error**: `bg-primary` doesn't apply styles

**Cause**: Missing `@theme inline` mapping

**Solution**:

@theme inline {

--color-background: var(--background);

--color-foreground: var(--foreground);

--color-primary: var(--primary);

/ ... map ALL CSS variables /

}


### 3. ❌ Dark Mode Not Switching

**Error**: Theme stays light/dark

**Cause**: Missing ThemeProvider

**Solution**:

- Create ThemeProvider (see `templates/theme-provider.tsx`)

- Wrap app in `main.tsx`

- Verify `.dark` class toggles on `<html>` element

### 4. ❌ Duplicate @layer base

**Error**: "Duplicate @layer base" in console

**Cause**: shadcn init adds `@layer base` - don't add another

**Solution**:

/ ✅ Correct - single @layer base /

@import "tailwindcss";

:root { --background: hsl(0 0% 100%); }

@theme inline { --color-background: var(--background); }

@layer base { body { background-color: var(--background); } }


### 5. ❌ Build Fails with tailwind.config.ts

**Error**: "Unexpected config file"

**Cause**: v4 doesn't use `tailwind.config.ts` (v3 legacy)

**Solution**:

rm tailwind.config.ts


v4 configuration happens in `src/index.css` using `@theme` directive.

### 6. ❌ @theme inline Breaks Dark Mode in Multi-Theme Setups

**Error**: Dark mode doesn't switch when using `@theme inline` with custom variants (e.g., `data-mode="dark"`)
**Source**: [GitHub Discussion #18560](https://github.com/tailwindlabs/tailwindcss/discussions/18560)

**Cause**: `@theme inline` bakes variable VALUES into utilities at build time. When dark mode changes the underlying CSS variables, utilities don't update because they reference hardcoded values, not variables.

**Why It Happens**:

- `@theme inline` inlines VALUES at build time: `bg-primary` → `background-color: oklch(...)`

- Dark mode overrides change the CSS variables, but utilities already have baked-in values

- The CSS specificity chain breaks

**Solution**: Use `@theme` (without inline) for multi-theme scenarios:

/ ✅ CORRECT - Use @theme without inline /

@custom-variant dark (&#x26;:where([data-mode=dark], [data-mode=dark] *));

@theme {

--color-text-primary: var(--color-slate-900);

--color-bg-primary: var(--color-white);

}

@layer theme {

[data-mode="dark"] {

--color-text-primary: var(--color-white);

--color-bg-primary: var(--color-slate-900);

}

}


**When to use inline**:

- Single theme + dark mode toggle (like shadcn/ui default) ✅

- Referencing other CSS variables that don't change ✅

**When NOT to use inline**:

- Multi-theme systems (data-theme="blue" | "green" | etc.) ❌

- Dynamic theme switching beyond light/dark ❌

**Maintainer Guidance** (Adam Wathan):

"It's more idiomatic in v4 for the actual generated CSS to reference your theme variables. I would personally only use inline when things don't work without it."

### 7. ❌ @apply with @layer base/components (v4 Breaking Change)

**Error**: `Cannot apply unknown utility class: custom-button`
**Source**: [GitHub Discussion #17082](https://github.com/tailwindlabs/tailwindcss/discussions/17082)

**Cause**: In v3, classes defined in `@layer base` and `@layer components` could be used with `@apply`. In v4, this is a breaking architectural change.

**Why It Happens**: v4 doesn't "hijack" the native CSS `@layer` at-rule anymore. Only classes defined with `@utility` are available to `@apply`.

**Migration**:

/ ❌ v3 pattern (worked) /

@layer components {

.custom-button {

@apply px-4 py-2 bg-blue-500;

}

}

/ ✅ v4 pattern (required) /

@utility custom-button {

@apply px-4 py-2 bg-blue-500;

}

/ OR use native CSS /

@layer base {

.custom-button {

padding: 1rem 0.5rem;

background-color: theme(colors.blue.500);

}

}


**Note**: This skill already discourages `@apply` usage. This error is primarily for users migrating from v3.

### 8. ❌ @layer base Styles Not Applying

**Error**: Styles defined in `@layer base` seem to be ignored
**Source**: [GitHub Discussion #16002](https://github.com/tailwindlabs/tailwindcss/discussions/16002) | [Discussion #18123](https://github.com/tailwindlabs/tailwindcss/discussions/18123)

**Cause**: v4 uses native CSS layers. Base styles CAN be overridden by utility layers due to CSS cascade if layers aren't explicitly ordered.

**Why It Happens**:

- v3: Tailwind intercepted `@layer base/components/utilities` and processed them specially

- v4: Uses native CSS layers - if you don't import layers in the right order, precedence breaks

- Styles ARE being applied, but utilities override them

**Solution Option 1**: Define layers explicitly:

@import "tailwindcss/theme.css" layer(theme);

@import "tailwindcss/base.css" layer(base);

@import "tailwindcss/components.css" layer(components);

@import "tailwindcss/utilities.css" layer(utilities);

@layer base {

body {

background-color: var(--background);

}

}


**Solution Option 2** (Recommended): Don't use `@layer base` - define styles at root level:

@import "tailwindcss";

:root {

--background: hsl(0 0% 100%);

}

body {

background-color: var(--background); / No @layer needed /

}


**Applies to**: ALL base styles, not just color variables. Avoid wrapping ANY styles in `@layer base` unless you understand CSS layer ordering.

## Quick Reference

Symptom
Cause
Fix

`bg-primary` doesn't work
Missing `@theme inline`
Add `@theme inline` block

Colors all black/white
Double `hsl()` wrapping
Use `var(--color)` not `hsl(var(--color))`

Dark mode not switching
Missing ThemeProvider
Wrap app in `<ThemeProvider>`

Build fails
`tailwind.config.ts` exists
Delete file

Animation errors
Using `tailwindcss-animate`
Install `tw-animate-css`

## What's New in Tailwind v4

### OKLCH Color Space (December 2024)

Tailwind v4.0 replaced the entire default color palette with OKLCH, a perceptually uniform color space.
**Source**: [Tailwind v4.0 Release](https://tailwindcss.com/blog/tailwindcss-v4) | [OKLCH Migration Guide](https://andy-cinquin.com/blog/migration-oklch-tailwind-css-4-0)

**Why OKLCH**:

- **Perceptual consistency**: HSL's "50% lightness" is visually inconsistent across hues (yellow appears much brighter than blue at same lightness)

- **Better gradients**: Smooth transitions without muddy middle colors

- **Wider gamut**: Supports colors beyond sRGB on modern displays

- **More vibrant colors**: Eye-catching, saturated colors previously limited by sRGB

**Browser Support** (January 2026):

- Chrome 111+, Firefox 113+, Safari 15.4+, Edge 111+

- Global coverage: 93.1%

**Automatic Fallbacks**: Tailwind generates sRGB fallbacks for older browsers:

.bg-blue-500 {

background-color: #3b82f6; / sRGB fallback /

background-color: oklch(0.6 0.24 264); / Modern browsers /

}


**Custom Colors**: When defining custom colors, OKLCH is now preferred:

@theme {

/ Modern approach (preferred) /

--color-brand: oklch(0.7 0.15 250);

/ Legacy approach (still works) /

--color-brand: hsl(240 80% 60%);

}


**Migration**: No breaking changes - Tailwind generates fallbacks automatically. For new projects, use OKLCH-aware tooling for custom colors.

### Built-in Features (No Plugin Needed)

**Container Queries** (built-in as of v4.0):

<div className="@container">

<div className="@md:text-lg @lg:grid-cols-2">

Content responds to container width, not viewport

</div>

</div>


**Line Clamp** (built-in as of v3.3):

<p className="line-clamp-3">Truncate to 3 lines with ellipsis...</p>

<p className="line-clamp-[8]">Arbitrary values supported</p>

<p className="line-clamp-(--teaser-lines)">CSS variable support</p>


**Removed Plugins**:

- `@tailwindcss/container-queries` - Built-in now

- `@tailwindcss/line-clamp` - Built-in since v3.3

## Tailwind v4 Plugins

Use `@plugin` directive (NOT `require()` or `@import`):

**Typography** (for Markdown/CMS content):

pnpm add -D @tailwindcss/typography

@import "tailwindcss";

@plugin "@tailwindcss/typography";

<article class="prose dark:prose-invert">{{ content }}</article>


**Forms** (cross-browser form styling):

pnpm add -D @tailwindcss/forms

@import "tailwindcss";

@plugin "@tailwindcss/forms";


**Container Queries** (built-in, no plugin needed):

<div className="@container">

<div className="@md:text-lg">Responds to container width</div>

</div>


**Common Plugin Errors**:

/ ❌ WRONG - v3 syntax /

@import "@tailwindcss/typography";

/ ✅ CORRECT - v4 syntax /

@plugin "@tailwindcss/typography";


## Setup Checklist

-  `@tailwindcss/vite` installed (NOT postcss)

-  `vite.config.ts` uses `tailwindcss()` plugin

-  `components.json` has `"config": ""`

-  NO `tailwind.config.ts` exists

-  `src/index.css` follows 4-step pattern:

-  `:root`/`.dark` at root level (not in @layer)

-  Colors wrapped with `hsl()`

-  `@theme inline` maps all variables

-  `@layer base` uses unwrapped variables

-  ThemeProvider wraps app

-  Theme toggle works

## File Templates

Available in `templates/` directory:

- **index.css** - Complete CSS with all color variables

- **components.json** - shadcn/ui v4 config

- **vite.config.ts** - Vite + Tailwind plugin

- **theme-provider.tsx** - Dark mode provider

- **utils.ts** - `cn()` utility

## Migration from v3

See `reference/migration-guide.md` for complete guide.

**Key Changes**:

- Delete `tailwind.config.ts`

- Move theme to CSS with `@theme inline`

- Replace `@tailwindcss/line-clamp` (now built-in: `line-clamp-*`)

- Replace `tailwindcss-animate` with `tw-animate-css`

- Update plugins: `require()` → `@plugin`

### Additional Migration Gotchas

#### Automated Migration Tool May Fail

**Warning**: The `@tailwindcss/upgrade` utility often fails to migrate configurations.
**Source**: [Community Reports](https://medium.com/better-dev-nextjs-react/tailwind-v4-migration-from-javascript-config-to-css-first-in-2025-ff3f59b215ca) | [GitHub Discussion #16642](https://github.com/tailwindlabs/tailwindcss/discussions/16642)

**Common failures**:

- Typography plugin configurations

- Complex theme extensions

- Custom plugin setups

**Recommendation**: Don't rely on automated migration. Follow manual steps in the migration guide instead.

#### Default Element Styles Removed

Tailwind v4 takes a more minimal approach to Preflight, removing default styles for headings, lists, and buttons.
**Source**: [GitHub Discussion #16517](https://github.com/tailwindlabs/tailwindcss/discussions/16517) | [Medium: Migration Problems](https://medium.com/better-dev-nextjs-react/tailwind-v4-migration-from-javascript-config-to-css-first-in-2025-ff3f59b215ca)

**Impact**:

- All headings (`<h1>` through `<h6>`) render at same size

- Lists lose default padding

- Visual regressions in existing projects

**Solutions**:

**Option 1: Use @tailwindcss/typography for content pages**:

pnpm add -D @tailwindcss/typography

@import "tailwindcss";

@plugin "@tailwindcss/typography";

<article className="prose dark:prose-invert">

{/ All elements styled automatically /}

</article>


**Option 2: Add custom base styles**:

@layer base {

h1 { @apply text-4xl font-bold mb-4; }

h2 { @apply text-3xl font-bold mb-3; }

h3 { @apply text-2xl font-bold mb-2; }

ul { @apply list-disc pl-6 mb-4; }

ol { @apply list-decimal pl-6 mb-4; }

}


#### PostCSS Setup Complexity

**Recommendation**: Use `@tailwindcss/vite` plugin for Vite projects instead of PostCSS.
**Source**: [Medium: Migration Problems](https://medium.com/better-dev-nextjs-react/tailwind-v4-migration-from-javascript-config-to-css-first-in-2025-ff3f59b215ca) | [GitHub Discussion #15764](https://github.com/tailwindlabs/tailwindcss/discussions/15764)

**Why Vite Plugin is Better**:

// ✅ Vite Plugin - One line, no PostCSS config

import tailwindcss from '@tailwindcss/vite'

export default defineConfig({

plugins: [react(), tailwindcss()],

})

// ❌ PostCSS - Multiple steps, plugin compatibility issues

// 1. Install @tailwindcss/postcss

// 2. Configure postcss.config.js

// 3. Manage plugin order

// 4. Debug plugin conflicts


**PostCSS Problems Reported**:

- Error: "It looks like you're trying to use tailwindcss directly as a PostCSS plugin"

- Multiple PostCSS plugins required: `postcss-import`, `postcss-advanced-variables`, `tailwindcss/nesting`

- v4 PostCSS plugin is separate package: `@tailwindcss/postcss`

**Official Guidance**: The Vite plugin is recommended for Vite projects. PostCSS is for legacy setups or non-Vite environments.

#### Visual Changes

**Ring Width Default**: Changed from 3px to 1px
**Source**: [Medium: Migration Guide](https://medium.com/better-dev-nextjs-react/tailwind-v4-migration-from-javascript-config-to-css-first-in-2025-ff3f59b215ca)

- `ring` class is now thinner

- Use `ring-3` to match v3 appearance

// v3: 3px ring

<button className="ring">Button</button>

// v4: 1px ring (thinner)

<button className="ring">Button</button>

// Match v3 appearance

<button className="ring-3">Button</button>

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card