React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

skillstailwindtailwind-v4shadcnthemingcss-variablesdark-modestyling

Tailwind v4 + shadcn/ui Skill - A Claude Code skill recipe for mastering Tailwind CSS v4 and shadcn/ui component styling

These skill recipes are designed for Claude Code but also work with other AI coding agents that support skill/instruction files.

Recipe

The complete SKILL.md content you can copy into .claude/skills/tailwind-v4-shadcn/SKILL.md:

---
name: tailwind-v4-shadcn
description: "Mastering Tailwind CSS v4 + shadcn/ui component architecture and styling. Use when asked to: tailwind help, shadcn component, styling, theming, dark mode, CSS variables, responsive design, animation, Tailwind v4 config."
allowed-tools: "Read, Write, Edit, Glob, Grep, Bash(npm:*), Bash(npx:*), Bash(pnpm:*), Agent"
---
 
# Tailwind v4 + shadcn/ui
 
You are a Tailwind CSS v4 and shadcn/ui expert. Provide authoritative guidance on styling, theming, and component architecture.
 
## Tailwind v4 Key Changes from v3
 
Tailwind v4 uses a CSS-first configuration model. There is no more tailwind.config.js by default.
 
### CSS-First Configuration
```css
/* app/globals.css */
@import "tailwindcss";
 
/* Custom theme values */
@theme \{
  --color-primary: oklch(0.7 0.15 240);
  --color-primary-foreground: oklch(0.98 0.01 240);
  --color-secondary: oklch(0.6 0.1 280);
  --color-secondary-foreground: oklch(0.98 0.01 280);
  --color-accent: oklch(0.8 0.12 160);
 
  --font-sans: "Inter", sans-serif;
  --font-mono: "JetBrains Mono", monospace;
 
  --radius-lg: 0.75rem;
  --radius-md: 0.5rem;
  --radius-sm: 0.25rem;
 
  --breakpoint-xs: 30rem;
 
  --animate-slide-in: slide-in 0.3s ease-out;
\}
 
@keyframes slide-in \{
  from \{ transform: translateY(10px); opacity: 0; \}
  to \{ transform: translateY(0); opacity: 1; \}
\}

Removed Features in v4

  • tailwind.config.js is optional (CSS-first via @theme)
  • @apply is discouraged (use component extraction instead)
  • theme() function replaced by CSS variables
  • Ring width default changed (ring-3 instead of ring)
  • bg-opacity-* replaced by bg-black/50 syntax

New in v4

  • Container queries built-in: @container, @sm, @md, @lg
  • 3D transforms: rotate-x-*, rotate-y-*, perspective-*
  • text-wrap-balance, text-wrap-pretty
  • field-sizing-content for auto-sizing textareas
  • color-scheme-* for system color schemes
  • Automatic dark mode via @media (prefers-color-scheme: dark)
  • Native CSS nesting support

shadcn/ui Architecture

Installation Pattern

npx shadcn@latest init
npx shadcn@latest add button card dialog form input

Component Customization Rules

  1. Edit the source - shadcn components live in your codebase at components/ui/. Modify them directly.
  2. Extend, do not wrap - Add variants to the component file, do not create wrapper components.
  3. Use CSS variables for theming - All colors should reference CSS variables for dark mode support.
  4. Compose with cn() - Use the cn() utility (clsx + twMerge) for conditional classes.

cn() Utility

import \{ clsx, type ClassValue \} from "clsx";
import \{ twMerge \} from "tailwind-merge";
 
export function cn(...inputs: ClassValue[]) \{
  return twMerge(clsx(inputs));
\}
 
// Usage
<div className=\{cn(
  "rounded-lg border p-4",
  isActive && "border-primary bg-primary/10",
  className // allow parent override
)\} />

Component Composition Pattern

// components/ui/card.tsx - shadcn base
const Card = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
  (\{ className, ...props \}, ref) => (
    <div
      ref=\{ref\}
      className=\{cn(
        "rounded-lg border bg-card text-card-foreground shadow-sm",
        className
      )\}
      \{...props\}
    />
  )
);
 
// Your composed component
function ProductCard(\{ product \}: \{ product: Product \}) \{
  return (
    <Card className="hover:shadow-md transition-shadow">
      <CardHeader>
        <CardTitle>\{product.name\}</CardTitle>
        <CardDescription>\{product.category\}</CardDescription>
      </CardHeader>
      <CardContent>
        <p className="text-2xl font-bold">$\{product.price\}</p>
      </CardContent>
      <CardFooter>
        <Button className="w-full">Add to Cart</Button>
      </CardFooter>
    </Card>
  );
\}

Theming with CSS Variables

shadcn Theme Setup

/* app/globals.css */
@import "tailwindcss";
 
@theme inline \{
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-card: var(--card);
  --color-card-foreground: var(--card-foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  --color-secondary: var(--secondary);
  --color-secondary-foreground: var(--secondary-foreground);
  --color-muted: var(--muted);
  --color-muted-foreground: var(--muted-foreground);
  --color-accent: var(--accent);
  --color-accent-foreground: var(--accent-foreground);
  --color-destructive: var(--destructive);
  --color-border: var(--border);
  --color-input: var(--input);
  --color-ring: var(--ring);
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);
\}
 
:root \{
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);
  --accent: oklch(0.97 0 0);
  --accent-foreground: oklch(0.205 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);
  --radius: 0.625rem;
\}
 
.dark \{
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.145 0 0);
  --card-foreground: oklch(0.985 0 0);
  --primary: oklch(0.985 0 0);
  --primary-foreground: oklch(0.205 0 0);
  --secondary: oklch(0.269 0 0);
  --secondary-foreground: oklch(0.985 0 0);
  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);
  --accent: oklch(0.269 0 0);
  --accent-foreground: oklch(0.985 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --border: oklch(0.269 0 0);
  --input: oklch(0.269 0 0);
  --ring: oklch(0.439 0 0);
\}

Dark Mode Toggle

"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-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
      <Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
      <span className="sr-only">Toggle theme</span>
    </Button>
  );
\}

Responsive Patterns

// Mobile-first responsive design
<div className="
  grid
  grid-cols-1        // mobile: single column
  sm:grid-cols-2     // 640px+: 2 columns
  lg:grid-cols-3     // 1024px+: 3 columns
  xl:grid-cols-4     // 1280px+: 4 columns
  gap-4
  p-4 sm:p-6 lg:p-8
">
 
// Container queries (new in v4)
<div className="@container">
  <div className="@sm:flex @sm:gap-4 @lg:grid @lg:grid-cols-3">
    \{/* Responds to container width, not viewport */\}
  </div>
</div>

Animation Patterns

// Tailwind built-in animations
<div className="animate-spin" />     // spinner
<div className="animate-pulse" />    // skeleton loading
<div className="animate-bounce" />   // attention
 
// Custom animations with @theme
// In globals.css:
// @theme \{ --animate-fade-in: fade-in 0.5s ease-out; \}
// @keyframes fade-in \{ from \{ opacity: 0; \} to \{ opacity: 1; \} \}
<div className="animate-fade-in" />
 
// Transition patterns
<button className="
  transition-colors duration-200
  bg-primary hover:bg-primary/90
  active:scale-95 transition-transform
" />
 
// For complex animations, use Framer Motion with Tailwind
import \{ motion \} from "framer-motion";
 
<motion.div
  initial=\{\{ opacity: 0, y: 20 \}\}
  animate=\{\{ opacity: 1, y: 0 \}\}
  className="rounded-lg border bg-card p-6"
/>

Common Styling Anti-Patterns

  1. Using @apply extensively - Extract components instead of @apply
  2. Hardcoding colors - Always use CSS variables or theme tokens
  3. Not using cn() - Class conflicts without twMerge cause unexpected styles
  4. Wrapping shadcn components - Modify the source file or use className prop
  5. Ignoring dark mode - Always test with both themes; use semantic color names
  6. Using arbitrary values too often - w-[347px] is a smell; use the design scale
  7. Inline styles alongside Tailwind - Pick one system; mixing causes maintenance burden

## Working Example

### Example 1: User asks "Help me set up theming for my app"

**User prompt:** "I want a blue-themed app with dark mode support using shadcn/ui."

**Skill-guided response would produce:**
- CSS variables for a blue color palette in oklch
- Light and dark theme definitions
- next-themes provider setup
- Theme toggle component
- Example components using the theme tokens

### Example 2: User asks "How do I customize a shadcn Button?"

**User prompt:** "I want to add a 'gradient' variant to the shadcn Button."

**Skill-guided response would produce:**

```tsx
// components/ui/button.tsx - Add to existing variants
const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring 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",
        // NEW: gradient variant
        gradient: "bg-gradient-to-r from-primary to-accent text-primary-foreground hover:opacity-90",
      },
      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",
    },
  }
);

Deep Dive

How the Skill Works

This skill provides:

  1. Tailwind v4 migration knowledge - All changes from v3 to v4 and how to configure
  2. shadcn/ui architecture - Component customization, composition, and theming patterns
  3. Theming system - CSS variable setup for light/dark mode with oklch colors
  4. Anti-pattern detection - Common styling mistakes and how to fix them

Customization

  • Add your project's design tokens and color palette
  • Include your custom component variants
  • Specify animation libraries used (Framer Motion, etc.)
  • Add brand-specific styling guidelines

How to Install

mkdir -p .claude/skills/tailwind-v4-shadcn
# Paste the Recipe content into .claude/skills/tailwind-v4-shadcn/SKILL.md

Gotchas

  • Tailwind v4 uses @theme, not tailwind.config.js - You can still use a config file, but CSS-first is the default and recommended approach.
  • oklch colors may not work in older browsers - Include fallbacks for IE/older Safari if needed.
  • shadcn components are copied into your project - They are not installed from npm. Updates must be applied manually or by re-running the add command.
  • twMerge must be configured for custom classes - If you add custom Tailwind classes, configure tailwind-merge to recognize them.
  • Container queries need @container on the parent - Without it, @sm: and similar queries will not work.
  • Dark mode class strategy - shadcn uses class strategy (.dark class on html element), not media (prefers-color-scheme). Make sure next-themes is configured to match.

Alternatives

ApproachWhen to Use
CSS ModulesComponent-scoped styles without utility classes
Styled ComponentsCSS-in-JS with dynamic theming
Panda CSSType-safe CSS-in-JS with build-time extraction
Vanilla ExtractZero-runtime CSS-in-TypeScript
Radix ThemesPre-built component library (less customizable than shadcn)
Park UIshadcn-like components for Ark UI

FAQs

What is the biggest configuration change from Tailwind v3 to v4?
  • Tailwind v4 uses a CSS-first configuration model via @theme in your CSS file
  • tailwind.config.js is optional and no longer the default
  • Custom theme values (colors, fonts, radii, breakpoints) are defined directly in CSS
How do you define custom theme values in Tailwind v4?
@import "tailwindcss";
 
@theme {
  --color-primary: oklch(0.7 0.15 240);
  --font-sans: "Inter", sans-serif;
  --radius-lg: 0.75rem;
  --animate-slide-in: slide-in 0.3s ease-out;
}
  • Use the @theme directive inside your CSS file
  • CSS custom properties follow the pattern --category-name: value
What features were removed or changed in Tailwind v4?
  • @apply is discouraged (use component extraction instead)
  • theme() function replaced by CSS variables
  • bg-opacity-* replaced by slash syntax like bg-black/50
  • Ring width default changed (ring-3 instead of ring)
What is the cn() utility and why is it important for shadcn/ui?
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
 
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
  • Combines clsx (conditional classes) with twMerge (resolves Tailwind conflicts)
  • Without twMerge, conflicting classes like p-4 and p-6 would both apply
How should you customize shadcn/ui components?
  • Edit the source files directly in components/ui/ -- they live in your codebase
  • Extend by adding variants to the component file, do not create wrapper components
  • Use CSS variables for theming and cn() for conditional classes
How does dark mode work with shadcn/ui and Tailwind v4?
  • shadcn uses the class strategy (.dark class on the html element), not media (prefers-color-scheme)
  • Define light variables in :root and dark overrides in .dark
  • Use next-themes for the toggle, configured to match the class strategy
Gotcha: Why might container queries not work with @sm: and @lg: prefixes?
  • Container queries require @container on the parent element
  • Without the @container class on the parent, @sm:, @md:, @lg: will not respond
  • This is a common oversight when adopting Tailwind v4 container queries
What new CSS features are built into Tailwind v4?
  • Container queries: @container, @sm, @md, @lg
  • 3D transforms: rotate-x-*, rotate-y-*, perspective-*
  • text-wrap-balance, text-wrap-pretty
  • field-sizing-content for auto-sizing textareas
  • Native CSS nesting support
Gotcha: What happens if you add custom Tailwind classes but do not configure tailwind-merge?
  • twMerge will not recognize your custom classes
  • Conflicting custom classes may not be resolved correctly
  • You need to configure tailwind-merge to recognize any custom utility classes you define
How do you type the className prop for a shadcn-style component in TypeScript?
import { cn } from "@/lib/utils";
 
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
  variant?: "default" | "outlined";
}
 
function Card({ className, variant, ...props }: CardProps) {
  return (
    <div
      className={cn("rounded-lg bg-card p-4", className)}
      {...props}
    />
  );
}
  • Extend React.HTMLAttributes<HTMLDivElement> to inherit className and all native div props
What are the common styling anti-patterns to avoid?
  • Using @apply extensively instead of extracting components
  • Hardcoding color values instead of using CSS variables or theme tokens
  • Not using cn() which causes class conflicts
  • Wrapping shadcn components instead of modifying the source
  • Using arbitrary values like w-[347px] too often instead of the design scale