# Tailwind Expert

> Tailwind CSS v4 styling rules, CSS-first design, theme customization, and utility optimization. Use whenever the user is writing or reviewing Tailwind CSS code, configuring themes, or migrating from Tailwind v3 to v4. Trigger on any mention of Tailwind, @theme, utility classes, CSS-first config, or Tailwind v3-to-v4 migration. Also trigger for questions about Tailwind performance, purging, or custom design tokens.

- Skill: `roedyrustam/tailwind-expert-2` (Agent Skill)
- Install (CLI): `npx skillmds@latest add roedyrustam/tailwind-expert-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/roedyrustam/tailwind-expert-2/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: roedyrustam (https://skillmd.com/u/roedyrustam)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/roedyrustam/tailwind-expert-2

---


# Tailwind CSS v4 Expert

CSS-first configuration, theme customization, and utility-first best practices.

---

## v4 — The Big Shift: CSS-First Configuration

Tailwind v4 moves configuration from `tailwind.config.js` into CSS using `@theme`.

```css
/* app/globals.css */
@import "tailwindcss";

@theme {
  /* Colors */
  --color-brand-50: oklch(0.97 0.02 260);
  --color-brand-500: oklch(0.55 0.22 260);
  --color-brand-900: oklch(0.25 0.12 260);

  /* Typography */
  --font-sans: "Inter Variable", system-ui, sans-serif;
  --font-display: "Cal Sans", sans-serif;

  /* Spacing base (multiplied by Tailwind's scale) */
  --spacing: 0.25rem;

  /* Border radius */
  --radius-card: 0.75rem;
  --radius-button: 0.5rem;

  /* Shadows */
  --shadow-card: 0 4px 24px oklch(0 0 0 / 0.08);
  --shadow-elevated: 0 8px 32px oklch(0 0 0 / 0.12);

  /* Custom breakpoints */
  --breakpoint-3xl: 1920px;

  /* Animations */
  --animate-fade-in: fade-in 0.2s ease-out;
  --animate-slide-up: slide-up 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes slide-up {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}
```

### Using Theme Values in Components
```tsx
// All these are auto-generated from @theme above
<div className="bg-brand-500 text-white rounded-card shadow-card animate-fade-in">
  <h1 className="font-display text-3xl">Welcome</h1>
</div>

<div className="3xl:grid-cols-6 grid grid-cols-2">
  {/* Custom breakpoint just works */}
</div>
```

---

## v3 → v4 Migration Guide

### Breaking Changes
```diff
- // tailwind.config.js (v3)
- module.exports = {
-   theme: {
-     extend: {
-       colors: { brand: { 500: '#3b82f6' } }
-     }
-   }
- }

+ /* globals.css (v4) */
+ @import "tailwindcss";
+ @theme {
+   --color-brand-500: #3b82f6;
+ }
```

```diff
- <div className="bg-opacity-50 bg-black">
+ <div className="bg-black/50">
```

```diff
- <div className="ring-2 ring-offset-2 ring-offset-white">
+ <div className="outline outline-2 outline-offset-2">
```

```diff
- // PostCSS config (v3) needed autoprefixer, postcss-import explicitly
- module.exports = { plugins: { tailwindcss: {}, autoprefixer: {} } }

+ // v4 — single plugin, no autoprefixer needed (built-in)
+ export default { plugins: { '@tailwindcss/postcss': {} } }
```

### Automated Migration
```bash
npx @tailwindcss/upgrade
```

---

## Dark Mode

```css
/* v4 — dark mode variant works out of the box */
@import "tailwindcss";

/* Custom dark mode selector (default uses media query) */
@custom-variant dark (&:where(.dark, .dark *));
```

```tsx
// Component using dark: variant
<div className="bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-50">
  Content adapts automatically
</div>

// Theme toggle (class-based)
function ThemeToggle() {
  const [isDark, setIsDark] = useState(false)

  useEffect(() => {
    document.documentElement.classList.toggle('dark', isDark)
  }, [isDark])

  return <button onClick={() => setIsDark(!isDark)}>Toggle theme</button>
}
```

---

## Component Patterns

### Variant-Based Components with CVA
```tsx
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

const badgeVariants = cva(
  "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
  {
    variants: {
      variant: {
        default: "bg-brand-100 text-brand-900",
        success: "bg-green-100 text-green-900",
        warning: "bg-amber-100 text-amber-900",
        danger: "bg-red-100 text-red-900",
      },
    },
    defaultVariants: { variant: "default" },
  }
)

interface BadgeProps
  extends React.HTMLAttributes<HTMLSpanElement>,
    VariantProps<typeof badgeVariants> {}

export function Badge({ className, variant, ...props }: BadgeProps) {
  return <span className={cn(badgeVariants({ variant }), className)} {...props} />
}
```

### `cn()` Utility (Required for Every Project)
```typescript
// lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

// Why both clsx AND twMerge:
// clsx handles conditional classes
// twMerge resolves Tailwind class CONFLICTS (e.g. "p-2 p-4" → "p-4")
cn("p-2", isActive && "p-4", className) // correctly resolves to p-4 if both present
```

---

## Responsive Design Patterns

```tsx
// Mobile-first — base styles apply to smallest screens
<div className="
  grid grid-cols-1 gap-4
  sm:grid-cols-2
  lg:grid-cols-3
  xl:grid-cols-4
">

// Container queries (v4 supports natively)
<div className="@container">
  <div className="@sm:flex @lg:grid @lg:grid-cols-2">
    {/* Responds to PARENT container size, not viewport */}
  </div>
</div>
```

---

## Performance & Optimization

### Avoid Arbitrary Values When a Theme Token Exists
```tsx
// ❌ Arbitrary value — not reusable, not in design system
<div className="text-[#3b82f6] p-[18px]">

// ✅ Theme token — consistent, reusable, themeable
<div className="text-brand-500 p-4">
```

### Group Related Utilities with `@apply` Sparingly
```css
/* Use @apply only for truly repeated, complex patterns */
.btn-base {
  @apply inline-flex items-center justify-center rounded-lg font-medium
         transition-colors focus-visible:outline-none focus-visible:ring-2
         disabled:pointer-events-none disabled:opacity-50;
}

/* Prefer component-level composition (CVA) over @apply for variants */
```

### Content Detection (Automatic in v4)
```css
/* v4 auto-detects content — no manual content[] config needed in most cases */
@import "tailwindcss";

/* Only needed for non-standard file locations */
@source "../node_modules/@acme/ui/**/*.{js,jsx}";
```

---

## Common Utility Recipes

```tsx
// Truncate text with ellipsis
<p className="truncate">Long text that gets cut off...</p>

// Multi-line truncate
<p className="line-clamp-3">Long text across multiple lines...</p>

// Aspect ratio containers
<div className="aspect-video bg-zinc-100 rounded-lg overflow-hidden">
  <img className="h-full w-full object-cover" />
</div>

// Sticky header with backdrop blur
<header className="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b">

// Scroll snap carousel
<div className="flex overflow-x-auto snap-x snap-mandatory gap-4">
  <div className="snap-center shrink-0 w-80">Card 1</div>
</div>

// Custom scrollbar (v4 supports natively)
<div className="overflow-y-auto [&::-webkit-scrollbar]:w-2
  [&::-webkit-scrollbar-thumb]:bg-zinc-300 [&::-webkit-scrollbar-thumb]:rounded-full">
```

---

## Key Rules

1. **Configure in CSS via `@theme`** — not `tailwind.config.js` (v4 paradigm)
2. **Use `bg-black/50` slash syntax** — not `bg-opacity-50` (removed in v4)
3. **Theme tokens over arbitrary values** — `text-brand-500` not `text-[#3b82f6]`
4. **`cn()` with `clsx` + `tailwind-merge`** in every project — handles conditional + conflicting classes
5. **CVA for variant-based components** — not manual ternaries for every variant
6. **Mobile-first breakpoints** — base styles = mobile, then `sm:`, `md:`, `lg:`
7. **Container queries (`@container`)** for component-level responsiveness
8. **`@apply` sparingly** — prefer composing utility classes directly in JSX
9. **`npx @tailwindcss/upgrade`** for v3→v4 migration — don't migrate by hand
10. **No `autoprefixer` needed** — v4's PostCSS plugin handles vendor prefixes

