| name | tailwind-expert |
| description | 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.
|
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.
@import "tailwindcss";
@theme {
--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);
--font-sans: "Inter Variable", system-ui, sans-serif;
--font-display: "Cal Sans", sans-serif;
--spacing: 0.25rem;
--radius-card: 0.75rem;
--radius-button: 0.5rem;
--shadow-card: 0 4px 24px oklch(0 0 0 / 0.08);
--shadow-elevated: 0 8px 32px oklch(0 0 0 / 0.12);
--breakpoint-3xl: 1920px;
--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
<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
- // tailwind.config.js (v3)
- module.exports = {
- theme: {
- extend: {
- colors: { brand: { 500: '#3b82f6' } }
- }
- }
- }
+ /* globals.css (v4) */
+ @import "tailwindcss";
+ @theme {
+ --color-brand-500: #3b82f6;
+ }
- <div className="bg-opacity-50 bg-black">
+ <div className="bg-black/50">
- <div className="ring-2 ring-offset-2 ring-offset-white">
+ <div className="outline outline-2 outline-offset-2">
- // 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
npx @tailwindcss/upgrade
Dark Mode
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
<div className="bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-50">
Content adapts automatically
</div>
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
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)
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
cn("p-2", isActive && "p-4", className)
Responsive Design Patterns
<div className="
grid grid-cols-1 gap-4
sm:grid-cols-2
lg:grid-cols-3
xl:grid-cols-4
">
<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
<div className="text-[#3b82f6] p-[18px]">
<div className="text-brand-500 p-4">
Group Related Utilities with @apply Sparingly
.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;
}
Content Detection (Automatic in v4)
@import "tailwindcss";
@source "../node_modules/@acme/ui/**/*.{js,jsx}";
Common Utility Recipes
<p className="truncate">Long text that gets cut off...</p>
<p className="line-clamp-3">Long text across multiple lines...</p>
<div className="aspect-video bg-zinc-100 rounded-lg overflow-hidden">
<img className="h-full w-full object-cover" />
</div>
<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
- Configure in CSS via
@theme — not tailwind.config.js (v4 paradigm)
- Use
bg-black/50 slash syntax — not bg-opacity-50 (removed in v4)
- Theme tokens over arbitrary values —
text-brand-500 not text-[#3b82f6]
cn() with clsx + tailwind-merge in every project — handles conditional + conflicting classes
- CVA for variant-based components — not manual ternaries for every variant
- Mobile-first breakpoints — base styles = mobile, then
sm:, md:, lg:
- Container queries (
@container) for component-level responsiveness
@apply sparingly — prefer composing utility classes directly in JSX
npx @tailwindcss/upgrade for v3→v4 migration — don't migrate by hand
- No
autoprefixer needed — v4's PostCSS plugin handles vendor prefixes