---
name: tailwind-css
description: "Tailwind CSS v4: utility-first styling, responsive design, dark mode, custom themes, component patterns, and cn() utility. Use when styling or reviewing UI with Tailwind: utilities, responsive design, dark mode, theming."
tags: [tailwind, css, styling, responsive, dark-mode, frontend]
version: "2025.1"
---

# Tailwind CSS v4

## Core Concepts

Tailwind CSS v4 uses a CSS-first configuration approach. Configuration is done in CSS
using `@theme` instead of `tailwind.config.js`. The engine is rewritten in Rust for speed.
Content detection is automatic with no `content` array needed.

## Setup (v4)

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

@theme {
  --color-brand: #4f46e5;
  --color-brand-light: #818cf8;
  --color-brand-dark: #3730a3;

  --font-sans: "Inter", sans-serif;
  --font-mono: "JetBrains Mono", monospace;

  --breakpoint-xs: 475px;

  --spacing-18: 4.5rem;
  --spacing-128: 32rem;
}
```

## Responsive Design

```html
<!-- Mobile-first: base → sm → md → lg → xl → 2xl -->
<div class="
  grid grid-cols-1       /* Mobile: single column */
  sm:grid-cols-2         /* >= 640px: two columns */
  md:grid-cols-3         /* >= 768px: three columns */
  lg:grid-cols-4         /* >= 1024px: four columns */
  xl:grid-cols-5         /* >= 1280px: five columns */
  gap-4 p-4
">
  <div class="bg-white rounded-lg shadow p-6">Card</div>
</div>

<!-- Container queries (v4 native) -->
<div class="@container">
  <div class="@sm:flex @sm:items-center @sm:gap-4">
    <img class="w-full @sm:w-24 @sm:h-24 rounded-lg" src="..." alt="" />
    <div class="mt-2 @sm:mt-0">
      <h3 class="text-lg font-semibold">Title</h3>
      <p class="text-gray-600">Description</p>
    </div>
  </div>
</div>
```

## Dark Mode

```html
<!-- dark: prefix for dark mode variants -->
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
  <h1 class="text-2xl font-bold text-gray-800 dark:text-gray-200">
    Heading
  </h1>
  <p class="text-gray-600 dark:text-gray-400">
    Body text that adapts to dark mode.
  </p>
  <button class="
    bg-brand text-white
    hover:bg-brand-dark
    dark:bg-brand-light dark:text-gray-900
    dark:hover:bg-brand
    rounded-lg px-4 py-2 transition-colors
  ">
    Action
  </button>
</div>

<!-- Toggle dark mode with class strategy -->
<!-- In v4, use @variant or CSS custom property approach -->
```

## Component Patterns

### Card

```html
<article class="
  bg-white dark:bg-gray-800
  rounded-2xl shadow-md
  overflow-hidden
  transition-shadow hover:shadow-lg
">
  <img class="w-full h-48 object-cover" src="..." alt="..." />
  <div class="p-6">
    <span class="text-xs font-semibold uppercase tracking-wide text-brand">
      Category
    </span>
    <h3 class="mt-2 text-lg font-bold text-gray-900 dark:text-gray-100">
      Card Title
    </h3>
    <p class="mt-2 text-sm text-gray-600 dark:text-gray-400 line-clamp-3">
      Card description that may be long and gets truncated after three lines.
    </p>
    <div class="mt-4 flex items-center justify-between">
      <span class="text-sm text-gray-500">3 min read</span>
      <a href="#" class="text-sm font-medium text-brand hover:text-brand-dark">
        Read more &rarr;
      </a>
    </div>
  </div>
</article>
```

### Form

```html
<form class="space-y-6 max-w-md mx-auto">
  <div>
    <label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
      Email
    </label>
    <input
      type="email"
      id="email"
      class="
        mt-1 block w-full rounded-lg border border-gray-300
        px-4 py-2.5 text-gray-900
        shadow-sm
        placeholder:text-gray-400
        focus:border-brand focus:ring-2 focus:ring-brand/20 focus:outline-none
        dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100
        dark:focus:border-brand-light
      "
      placeholder="you@example.com"
    />
  </div>

  <div>
    <label for="password" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
      Password
    </label>
    <input
      type="password"
      id="password"
      class="
        mt-1 block w-full rounded-lg border border-gray-300
        px-4 py-2.5 text-gray-900
        shadow-sm
        focus:border-brand focus:ring-2 focus:ring-brand/20 focus:outline-none
        dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100
      "
    />
    <p class="mt-1 text-xs text-gray-500">Must be at least 8 characters.</p>
  </div>

  <button
    type="submit"
    class="
      w-full rounded-lg bg-brand px-4 py-2.5
      text-sm font-semibold text-white
      shadow-sm
      hover:bg-brand-dark focus:ring-2 focus:ring-brand/50 focus:outline-none
      disabled:opacity-50 disabled:cursor-not-allowed
      transition-colors
    "
  >
    Sign in
  </button>
</form>
```

### Navigation

```html
<nav class="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
  <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
    <div class="flex h-16 items-center justify-between">
      <div class="flex items-center gap-8">
        <a href="/" class="text-xl font-bold text-brand">Logo</a>
        <div class="hidden md:flex gap-6">
          <a href="#" class="text-sm font-medium text-gray-900 dark:text-gray-100 hover:text-brand">
            Home
          </a>
          <a href="#" class="text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-brand">
            Products
          </a>
          <a href="#" class="text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-brand">
            About
          </a>
        </div>
      </div>
      <button class="md:hidden p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800">
        <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
        </svg>
      </button>
    </div>
  </div>
</nav>
```

## The cn() Utility

```typescript
// lib/utils.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

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

// Usage in components
import { cn } from '@/lib/utils';

interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  className?: string;
  children: React.ReactNode;
}

function Button({ variant = 'primary', size = 'md', className, children }: ButtonProps) {
  return (
    <button
      className={cn(
        // Base styles
        'inline-flex items-center justify-center rounded-lg font-medium transition-colors',
        'focus:outline-none focus:ring-2 focus:ring-offset-2',
        // Variant styles
        {
          'bg-brand text-white hover:bg-brand-dark focus:ring-brand': variant === 'primary',
          'bg-gray-100 text-gray-900 hover:bg-gray-200 focus:ring-gray-400': variant === 'secondary',
          'bg-transparent text-gray-600 hover:bg-gray-100 focus:ring-gray-400': variant === 'ghost',
        },
        // Size styles
        {
          'px-3 py-1.5 text-xs': size === 'sm',
          'px-4 py-2 text-sm': size === 'md',
          'px-6 py-3 text-base': size === 'lg',
        },
        // Allow override
        className
      )}
    >
      {children}
    </button>
  );
}
```

## Tailwind v4 Specific Features

```css
/* @variant for custom variants */
@variant hocus (&:hover, &:focus);
@variant theme-dark (@media (prefers-color-scheme: dark));

/* @utility for custom utilities */
@utility scrollbar-hidden {
  scrollbar-width: none;
  &::-webkit-scrollbar {
    display: none;
  }
}

/* @source for explicit content paths when auto-detection misses files */
@source "../components/**/*.tsx";
```

## Do's

- Use Tailwind utility classes directly in markup; avoid @apply in most cases
- Use the `cn()` utility (clsx + tailwind-merge) for conditional and composable classes
- Design mobile-first, layer up with responsive prefixes
- Use semantic spacing scale consistently (p-4, p-6, gap-4, etc.)
- Use `@theme` in CSS for Tailwind v4 configuration
- Use `group-*` and `peer-*` for parent/sibling state styling
- Use `transition-*` for smooth state changes
- Keep class strings readable with line breaks in multi-line JSX attributes

## Don'ts

- Do not use `@apply` to recreate components; use component composition instead
- Do not fight Tailwind with `!important`  -  restructure class order or use `cn()`
- Do not create tailwind.config.js for v4  -  use CSS `@theme` directive
- Do not mix Tailwind with inline styles for the same property
- Do not use arbitrary values (`[17px]`) if a standard scale value is close enough
- Do not nest responsive classes inside each other (redundant, confusing)
- Do not forget dark mode when building components
- Do not use string interpolation for Tailwind classes (breaks JIT scanning)

## Troubleshooting

| Problem | Cause | Solution |
|---------|-------|----------|
| Classes not applying | Class not in scanned content | Check auto-detection or add `@source` directive |
| Conflicting classes | Two utilities set same property | Use `cn()` / `twMerge` to resolve conflicts |
| Dark mode not working | Missing dark variant or strategy | Check `@theme` or media query approach |
| Custom color not found | Missing `@theme` definition | Define custom values in `@theme` block |
| Container queries not working | Missing `@container` parent | Wrap parent element with `@container` class |
| Arbitrary value not compiled | Syntax error in brackets | Check for spaces: `w-[calc(100%-2rem)]` |
