Tailwind CSS v4 Best Practices Guide (2024-2025)

· combray's blog


Date: November 30, 2025 Author: Research Report Status: Current as of Tailwind CSS v4.0 stable release (January 22, 2025)


Executive Summary #

Tailwind CSS v4 represents a fundamental reimagining of the framework, transitioning from JavaScript-based configuration to a CSS-first architecture. Released in stable form on January 22, 2025, v4 delivers dramatic performance improvements (up to 10x faster builds), simplified setup, and leverages modern CSS features like cascade layers, @property, and color-mix().

This report covers migration strategies, the new @theme directive, project organization patterns, performance optimization, accessibility best practices, and integration with modern frameworks like Astro and React.


1. Tailwind v4 New Features and Migration from v3 #

1.1 Release Timeline #

1.2 Major New Features #

High-Performance Engine (Oxide) #

Tailwind v4 features a complete rewrite with critical components in Rust, delivering:

CSS-First Configuration #

The most significant change is the replacement of tailwind.config.js with CSS-based configuration using the @theme directive:

1@import "tailwindcss";
2
3@theme {
4  --font-display: "Satoshi", "sans-serif";
5  --breakpoint-3xl: 1920px;
6  --color-avocado-500: oklch(0.84 0.18 117.33);
7  --ease-fluid: cubic-bezier(0.3, 0, 0, 1);
8}

Benefits:

Simplified Installation #

Three simple steps replace previous complexity:

 1# 1. Install dependencies
 2npm i tailwindcss @tailwindcss/postcss
 3
 4# 2. Configure PostCSS
 5export default {
 6  plugins: ["@tailwindcss/postcss"],
 7};
 8
 9# 3. Import in CSS
10@import "tailwindcss";

For Vite projects, use the first-party plugin for maximum performance:

1npm i tailwindcss @tailwindcss/vite
1// vite.config.js
2import tailwindcss from "@tailwindcss/vite";
3
4export default {
5  plugins: [tailwindcss()],
6};

Automatic Content Detection #

Template files are discovered automatically using intelligent heuristics:

Dynamic Utility Values #

No more square brackets for many dynamic values:

1<!-- v3 -->
2<div class="h-[100px] grid-cols-[15]">
3
4<!-- v4 -->
5<div class="h-100 grid-cols-15">

Custom data attributes work natively:

1<div data-current class="opacity-75 data-current:opacity-100">

Container Queries in Core #

Built-in support without plugins:

1<div class="@container">
2  <div class="grid grid-cols-1 @sm:grid-cols-3">
3    <!-- Content adapts to container size, not viewport -->
4  </div>
5</div>

Max-width container queries:

1<div class="@max-sm:hidden">
2  <!-- Hidden when container is smaller than sm -->
3</div>

3D Transform Utilities #

Native 3D transform support:

1<div class="perspective-distant">
2  <article class="rotate-x-51 rotate-z-43 transform-3d">
3    <!-- 3D transformed content -->
4  </article>
5</div>

Enhanced Gradient APIs #

Linear gradients with angles:

1<div class="bg-linear-45 from-indigo-500 to-pink-500">

Gradient interpolation control:

1<div class="bg-linear-to-r/oklch from-indigo-500 to-teal-400">

Conic and radial gradients:

1<div class="bg-conic/[in_hsl_longer_hue] from-red-600 to-red-600">
2<div class="bg-radial-[at_25%_25%] from-white to-zinc-900">

@starting-style Support #

Animate elements as they appear without JavaScript:

1<div popover id="my-popover"
2     class="transition-discrete starting:open:opacity-0">
3  <!-- Animates in when popover opens -->
4</div>

not-* Variant #

Style elements that don't match conditions:

1<div class="not-hover:opacity-75">
2  <!-- Reduced opacity when NOT hovering -->
3</div>
4
5<div class="not-supports-hanging-punctuation:px-4">
6  <!-- Padding when feature not supported -->
7</div>

Additional Utilities #

Modernized Color Palette #

Colors upgraded from rgb to oklch color space for:

1.3 Breaking Changes #

Browser Requirements #

Minimum versions:

These requirements enable use of modern CSS features like @property and color-mix(). If you need older browser support, remain on v3.4.

Import Syntax #

v3:

1@tailwind base;
2@tailwind components;
3@tailwind utilities;

v4:

1@import "tailwindcss";

Utility Naming Changes #

Several utilities renamed for consistency:

v3 v4
shadow-sm shadow-xs
shadow shadow-sm
blur-sm blur-xs
rounded-sm rounded-xs
outline-none outline-hidden
ring (3px) ring-3

Default Color Changes #

Important Modifier Position #

v3:

1<div class="!flex !bg-red-500">

v4:

1<div class="flex! bg-red-500!">

Move ! to the end of class names (old way still supported but deprecated).

Prefix Syntax #

v3:

1<div class="flex:tw bg-blue-500:tw">

v4:

1<div class="tw:flex tw:bg-blue-500">

Prefixes now appear at the class start.

Custom Utilities #

v3:

1@layer utilities {
2  .custom-utility {
3    /* styles */
4  }
5}

v4:

1@utility custom-utility {
2  /* styles */
3}

Use @utility directive instead of @layer utilities.

CSS Variable Syntax #

v3:

1<div class="bg-[--brand-color]">

v4:

1<div class="bg-(--brand-color)">

Other Notable Changes #

1.4 Migration Strategy #

Automated Migration Tool #

The official upgrade tool automates most migration work:

1npx @tailwindcss/upgrade

Requirements:

What it does:

Recommendation: Run in a new branch and review changes before merging.

Manual Migration Steps #

1. Update PostCSS configuration:

1// postcss.config.js
2export default {
3  plugins: {
4    "@tailwindcss/postcss": {},
5  },
6};

2. Update Vite configuration (if using Vite):

1import tailwindcss from "@tailwindcss/vite";
2
3export default {
4  plugins: [tailwindcss()],
5};

3. Update CLI commands:

1# Old
2npx tailwindcss -i input.css -o output.css
3
4# New
5npx @tailwindcss/cli -i input.css -o output.css

4. Convert configuration:

Migrate tailwind.config.js theme settings to @theme in your CSS file.

5. Update imports:

Replace @tailwind directives with @import "tailwindcss".

6. Test thoroughly:


2. Theme Customization with @theme Directive #

2.1 Understanding @theme #

The @theme directive defines special CSS variables that:

  1. Create corresponding utility classes
  2. Become CSS custom properties accessible anywhere
  3. Integrate seamlessly with Tailwind's design system

Key distinction: Unlike :root variables, @theme variables generate utilities.

2.2 Basic Usage #

1@import "tailwindcss";
2
3@theme {
4  --color-mint-500: oklch(0.72 0.11 178);
5  --font-script: Great Vibes, cursive;
6  --spacing-huge: 12rem;
7}

This generates:

2.3 Extending vs. Overriding #

Extending the Default Theme #

Add new variables alongside defaults:

1@theme {
2  --font-poppins: Poppins, sans-serif;
3  --color-brand-purple: oklch(0.55 0.25 285);
4}

Default utilities remain available; your custom ones are added.

Overriding Specific Values #

Replace individual default values:

1@theme {
2  --breakpoint-sm: 30rem;  /* Changed from default 40rem */
3  --breakpoint-lg: 64rem;  /* Changed from default 64rem */
4}

Complete Namespace Replacement #

Remove all defaults in a namespace:

1@theme {
2  --color-*: initial;
3  --color-white: #fff;
4  --color-black: #000;
5  --color-purple: #3f3cbb;
6  --color-pink: #ec4899;
7}

Only your custom colors generate utilities; all default colors removed.

Custom Theme from Scratch #

1@theme {
2  --*: initial;
3  --spacing: 4px;
4  --font-body: Inter, sans-serif;
5  --color-primary: oklch(0.72 0.11 221.19);
6  --breakpoint-sm: 640px;
7  --breakpoint-md: 768px;
8}

2.4 Common Namespaces #

Namespace Maps To Example Utilities
--color-* Color utilities bg-red-500, text-blue-700
--font-* Font families font-sans, font-mono
--text-* Font sizes text-xl, text-sm
--font-weight-* Font weights font-bold, font-light
--spacing-* Spacing utilities p-4, m-8, gap-2
--breakpoint-* Responsive variants sm:*, md:*, lg:*
--shadow-* Shadow utilities shadow-lg, shadow-md
--radius-* Border radius rounded-lg, rounded-full

2.5 Advanced Features #

Animation Keyframes #

 1@theme {
 2  --animate-fade-in-scale: fade-in-scale 0.3s ease-out;
 3
 4  @keyframes fade-in-scale {
 5    0% {
 6      opacity: 0;
 7      transform: scale(0.95);
 8    }
 9    100% {
10      opacity: 1;
11      transform: scale(1);
12    }
13  }
14}

Usage:

1<div class="animate-fade-in-scale">
2  <!-- Smoothly fades in and scales up -->
3</div>

Variable References with inline #

1@theme inline {
2  --font-sans: var(--font-inter);
3  --color-primary: var(--color-purple-600);
4}

The inline option ensures utilities use actual values, not nested references.

Shared Theme Files #

./brand/theme.css:

1@theme {
2  --color-primary: oklch(0.72 0.11 221.19);
3  --color-secondary: oklch(0.85 0.15 142.50);
4  --font-body: Inter, sans-serif;
5  --font-heading: Poppins, sans-serif;
6}

./app.css:

1@import "tailwindcss";
2@import "../brand/theme.css";

This enables theme reuse across multiple projects.

2.6 Using Theme Variables in Custom CSS #

All theme variables are accessible as CSS variables:

 1@layer components {
 2  .card {
 3    background: var(--color-white);
 4    border: 1px solid var(--color-gray-200);
 5    border-radius: var(--radius-lg);
 6    padding: var(--spacing-4);
 7  }
 8
 9  .typography p {
10    font-size: var(--text-base);
11    color: var(--color-gray-700);
12  }
13}

In JavaScript:

1const styles = getComputedStyle(document.documentElement);
2const shadowValue = styles.getPropertyValue("--shadow-xl");

In arbitrary values with calculations:

1<div class="rounded-[calc(var(--radius-xl)-1px)]">
2  <!-- Calculated border radius -->
3</div>

2.7 Dark Mode with @theme #

Using light-dark() Function #

1@theme {
2  --color-background: light-dark(#ffffff, #0a0a0a);
3  --color-foreground: light-dark(#0a0a0a, #ffffff);
4  --color-pink: light-dark(#eb6bd8, #8e0d7a);
5}

Note: light-dark() is Baseline 2024, so browser support is Safari 17.5+, Chrome 123+, Firefox 120+.

Custom Dark Mode Variant #

1@custom-variant dark (&:where(.dark, .dark *));

Then toggle with JavaScript:

1// Add to <html> or <body>
2document.documentElement.classList.toggle('dark');

Define semantic tokens once:

 1@theme {
 2  --color-background: oklch(1 0 0);
 3  --color-foreground: oklch(0.2 0 0);
 4  --color-card: oklch(0.98 0 0);
 5  --color-border: oklch(0.9 0 0);
 6}
 7
 8@media (prefers-color-scheme: dark) {
 9  @theme {
10    --color-background: oklch(0.15 0 0);
11    --color-foreground: oklch(0.95 0 0);
12    --color-card: oklch(0.18 0 0);
13    --color-border: oklch(0.25 0 0);
14  }
15}

Benefits:

2.8 When to Use @theme vs :root #

Use @theme when:

Use :root when:

Example:

1@theme {
2  --color-primary: oklch(0.72 0.11 221.19);
3}
4
5:root {
6  --header-height: 64px;  /* No utility needed */
7  --sidebar-width: 280px;
8}

3. Project Organization and CSS Architecture #

project/
├── src/
│   ├── styles/
│   │   ├── main.css           # Main entry point
│   │   ├── theme.css          # @theme definitions
│   │   ├── base.css           # Base/reset styles
│   │   ├── components.css     # Component styles
│   │   └── utilities.css      # Custom utilities
│   ├── components/
│   │   ├── Button/
│   │   │   ├── Button.tsx
│   │   │   └── Button.module.css (optional)
│   │   └── Card/
│   │       ├── Card.tsx
│   │       └── Card.module.css (optional)
│   └── ...

3.2 CSS Layer Architecture #

Tailwind v4 uses native CSS cascade layers:

@layer theme, base, components, utilities;

Layer order (from lowest to highest specificity):

  1. theme - Design tokens and theme variables
  2. base - Reset styles and base element styles
  3. components - Component patterns
  4. utilities - Utility classes

Example main.css:

1@import "tailwindcss";
2@import "./theme.css";
3@import "./base.css";
4@import "./components.css";
5@import "./utilities.css";

theme.css:

1@theme {
2  --color-primary: oklch(0.72 0.11 221.19);
3  --color-secondary: oklch(0.85 0.15 142.50);
4  --font-body: Inter, sans-serif;
5}

base.css:

1@layer base {
2  body {
3    @apply font-body text-foreground bg-background;
4  }
5
6  h1, h2, h3 {
7    @apply font-heading;
8  }
9}

components.css:

 1@layer components {
 2  .btn {
 3    @apply px-4 py-2 rounded-lg font-medium transition-colors;
 4  }
 5
 6  .btn-primary {
 7    @apply bg-primary text-white hover:bg-primary-600;
 8  }
 9
10  .card {
11    @apply bg-card rounded-xl p-6 shadow-sm border border-border;
12  }
13}

utilities.css:

1@utility scrollbar-hide {
2  -ms-overflow-style: none;
3  scrollbar-width: none;
4
5  &::-webkit-scrollbar {
6    display: none;
7  }
8}

3.3 Component Extraction Best Practices #

Avoid Premature Abstraction #

Anti-pattern:

1/* Creating a class for something used once */
2@layer components {
3  .hero-heading {
4    @apply text-4xl font-bold text-gray-900;
5  }
6}

Better:

1<h1 class="text-4xl font-bold text-gray-900">
2  <!-- Used directly in template -->
3</h1>

Rule of thumb: Only extract when used 3+ times or when it represents a distinct component pattern.

Prefer Framework Components Over @apply #

Anti-pattern:

 1.btn-primary {
 2  @apply px-4 py-2 bg-blue-500 text-white rounded;
 3}
 4
 5.btn-secondary {
 6  @apply px-4 py-2 bg-gray-500 text-white rounded;
 7}
 8
 9.btn-large {
10  @apply px-6 py-3 text-lg;
11}

Better (React):

 1// Button.tsx
 2type ButtonProps = {
 3  variant?: 'primary' | 'secondary';
 4  size?: 'normal' | 'large';
 5  children: React.ReactNode;
 6};
 7
 8export function Button({
 9  variant = 'primary',
10  size = 'normal',
11  children
12}: ButtonProps) {
13  const baseClasses = 'rounded font-medium transition-colors';
14
15  const variantClasses = {
16    primary: 'bg-blue-500 text-white hover:bg-blue-600',
17    secondary: 'bg-gray-500 text-white hover:bg-gray-600',
18  };
19
20  const sizeClasses = {
21    normal: 'px-4 py-2 text-base',
22    large: 'px-6 py-3 text-lg',
23  };
24
25  return (
26    <button
27      className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`}
28    >
29      {children}
30    </button>
31  );
32}

Use @apply Strategically #

Good use cases:

Poor use cases:

3.4 Enterprise-Scale Organization #

Multi-Brand/Multi-Theme Setup #

Structure:

project/
├── themes/
│   ├── brand-a/
│   │   └── theme.css
│   ├── brand-b/
│   │   └── theme.css
│   └── shared/
│       └── base-theme.css
├── src/
│   ├── app-a/
│   │   └── main.css  # @import "../../themes/brand-a/theme.css"
│   └── app-b/
│       └── main.css  # @import "../../themes/brand-b/theme.css"

themes/brand-a/theme.css:

1@import "../shared/base-theme.css";
2
3@theme {
4  --color-primary: oklch(0.50 0.20 240);
5  --color-secondary: oklch(0.70 0.15 180);
6  --font-heading: "Brand A Sans", sans-serif;
7}

Design System Integration #

1. Define design tokens centrally:

 1@theme {
 2  /* Spacing scale */
 3  --spacing-0: 0;
 4  --spacing-1: 0.25rem;
 5  --spacing-2: 0.5rem;
 6  --spacing-3: 0.75rem;
 7  --spacing-4: 1rem;
 8  --spacing-6: 1.5rem;
 9  --spacing-8: 2rem;
10  --spacing-12: 3rem;
11  --spacing-16: 4rem;
12
13  /* Typography scale */
14  --text-xs: 0.75rem;
15  --text-sm: 0.875rem;
16  --text-base: 1rem;
17  --text-lg: 1.125rem;
18  --text-xl: 1.25rem;
19  --text-2xl: 1.5rem;
20  --text-3xl: 1.875rem;
21
22  /* Color palette */
23  --color-gray-50: oklch(0.99 0 0);
24  --color-gray-100: oklch(0.97 0 0);
25  /* ... */
26  --color-gray-900: oklch(0.25 0 0);
27}

2. Document design system:

Maintain a living style guide with:

3. Enforce with linting:

 1// .stylelintrc.json
 2{
 3  "rules": {
 4    "function-disallowed-list": ["rgb", "rgba", "hsl", "hsla"],
 5    "declaration-property-value-disallowed-list": {
 6      "color": ["/^#/"],
 7      "background-color": ["/^#/"]
 8    }
 9  }
10}

3.5 Scaling Strategies for Large Teams #

Plugin-Based Architecture #

Separate concerns by domain:

1// tailwind.config.js (if using legacy config)
2module.exports = {
3  plugins: [
4    require('./plugins/marketing'),
5    require('./plugins/dashboard'),
6    require('./plugins/forms'),
7  ],
8};

Or in CSS with separate imports:

1@import "tailwindcss";
2@import "./themes/marketing.css";
3@import "./themes/dashboard.css";
4@import "./themes/forms.css";

Automated Class Sorting #

Use Prettier with Tailwind plugin:

1npm i -D prettier prettier-plugin-tailwindcss

.prettierrc:

1{
2  "plugins": ["prettier-plugin-tailwindcss"]
3}

Classes automatically sort to Tailwind's recommended order.

Component Library Pattern #

Create a dedicated components package:

packages/
├── ui/
│   ├── src/
│   │   ├── Button/
│   │   ├── Card/
│   │   └── Input/
│   ├── styles/
│   │   └── theme.css
│   └── package.json
└── app/
    ├── src/
    └── package.json

Share components across projects while maintaining consistent styling.

3.6 Hybrid Architecture (Tailwind + CSS Modules) #

For complex component-specific styles:

Button.module.css:

 1.button {
 2  /* Complex styles that don't map well to utilities */
 3  background: linear-gradient(135deg,
 4    var(--color-primary) 0%,
 5    var(--color-secondary) 100%);
 6  box-shadow:
 7    0 1px 3px rgba(0, 0, 0, 0.1),
 8    inset 0 1px 0 rgba(255, 255, 255, 0.1);
 9}
10
11.button::before {
12  /* Pseudo-element styling */
13}

Button.tsx:

1import styles from './Button.module.css';
2
3export function Button({ children }) {
4  return (
5    <button className={`${styles.button} px-4 py-2 rounded-lg`}>
6      {children}
7    </button>
8  );
9}

When to use this pattern:


4. Performance Considerations #

4.1 Build Performance #

Tailwind v4 Benchmarks #

Tailwind CSS website:

Catalyst UI kit:

Incremental builds:

Optimization Strategies #

1. Use the Vite plugin for Vite projects:

1import tailwindcss from "@tailwindcss/vite";
2
3export default {
4  plugins: [tailwindcss()],
5};

The Vite plugin provides tighter integration and better caching.

2. Minimize custom utilities:

Every custom @utility adds to processing time. Prefer framework components.

3. Leverage automatic content detection:

Don't manually configure content paths unless necessary. Automatic detection is optimized.

4. Use native CSS features:

v4 leverages native cascade layers, @property, and color-mix() which browsers handle efficiently.

4.2 Runtime Performance #

CSS Bundle Size #

Most Tailwind projects ship less than 10KB of CSS to the client due to automatic purging of unused utilities.

Best practices:

1. Avoid dynamic class construction:

 1// ❌ Bad - Tailwind can't detect these classes
 2const color = 'red';
 3<div className={`bg-${color}-500`} />
 4
 5// ✅ Good - Full class names
 6const colorClasses = {
 7  red: 'bg-red-500',
 8  blue: 'bg-blue-500',
 9};
10<div className={colorClasses[color]} />

2. Use safelist for truly dynamic classes:

1@utility bg-red-500 {
2  /* Force inclusion */
3}
4
5@utility bg-blue-500 {
6  /* Force inclusion */
7}

3. Avoid unnecessary specificity:

1<!-- ❌ Over-specific -->
2<div class="text-gray-700 hover:text-gray-900 md:text-gray-800 md:hover:text-gray-950">
3
4<!-- ✅ More efficient -->
5<div class="text-gray-700 hover:text-gray-900">

Loading Strategy #

1. Inline critical CSS:

 1<!DOCTYPE html>
 2<html>
 3<head>
 4  <style>
 5    /* Inline critical above-the-fold styles */
 6    body { font-family: sans-serif; }
 7    .hero { /* ... */ }
 8  </style>
 9  <link rel="stylesheet" href="/styles.css">
10</head>

2. Use media attribute for conditional loading:

1<link rel="stylesheet" href="/print.css" media="print">

3. Preload fonts referenced in @theme:

1<link rel="preload" href="/fonts/inter.woff2" as="font" crossorigin>

4.3 Developer Experience Performance #

Fast Feedback Loop #

v4's incremental build speed (192µs for cached builds) means:

Automatic Content Detection Benefits #


5. Accessibility Patterns with Tailwind #

5.1 Built-in ARIA Support #

Tailwind v3.2+ includes variants for ARIA attributes:

1<button
2  role="switch"
3  aria-checked="true"
4  class="bg-gray-200 aria-checked:bg-blue-500"
5>
6  Toggle
7</button>

Common ARIA variants:

5.2 Accessible Toggle Switch Pattern #

 1import { useState } from 'react';
 2
 3export function ToggleSwitch({ label, defaultChecked = false }) {
 4  const [checked, setChecked] = useState(defaultChecked);
 5
 6  return (
 7    <label class="flex items-center gap-3">
 8      <span class="text-sm font-medium">{label}</span>
 9      <button
10        role="switch"
11        aria-checked={checked}
12        onClick={() => setChecked(!checked)}
13        className="
14          relative inline-flex h-6 w-11 items-center rounded-full
15          transition-colors
16          bg-gray-200 aria-checked:bg-blue-500
17          focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2
18        "
19      >
20        <span class="sr-only">{label}</span>
21        <span
22          className="
23            inline-block h-4 w-4 rounded-full bg-white
24            transition-transform
25            translate-x-1 aria-checked:translate-x-6
26          "
27          aria-hidden="true"
28        />
29      </button>
30    </label>
31  );
32}

5.3 Focus Management #

Visible Focus Indicators #

1<button class="
2  px-4 py-2 bg-blue-500 text-white rounded
3  focus-visible:outline-2 focus-visible:outline-blue-700 focus-visible:outline-offset-2
4">
5  Accessible Button
6</button>

Best practices:

 1<a
 2  href="#main-content"
 3  class="
 4    sr-only focus:not-sr-only
 5    focus:absolute focus:top-4 focus:left-4
 6    focus:z-50 focus:px-4 focus:py-2
 7    focus:bg-white focus:text-blue-700
 8    focus:rounded focus:shadow-lg
 9  "
10>
11  Skip to main content
12</a>
13
14<main id="main-content">
15  <!-- Content -->
16</main>

5.4 Screen Reader Utilities #

sr-only Class #

1<button>
2  <svg class="w-5 h-5" aria-hidden="true">
3    <!-- Icon -->
4  </svg>
5  <span class="sr-only">Close dialog</span>
6</button>

Conditional Screen Reader Text #

1<div class="flex items-center gap-2">
2  <span class="text-green-600"></span>
3  <span>Verified</span>
4  <span class="sr-only">account status</span>
5</div>

5.5 Group and Peer ARIA Variants #

 1<!-- Parent controls child styling based on ARIA state -->
 2<div class="group">
 3  <button aria-expanded="false" class="...">
 4    Accordion Header
 5  </button>
 6  <div class="hidden group-aria-expanded:block">
 7    Accordion content
 8  </div>
 9</div>
10
11<!-- Sibling controls styling -->
12<div>
13  <input
14    type="text"
15    aria-invalid="true"
16    class="peer border-gray-300 aria-invalid:border-red-500"
17  />
18  <p class="hidden peer-aria-invalid:block text-red-600 text-sm">
19    Please enter a valid value
20  </p>
21</div>

5.6 Motion Preferences #

1<div class="
2  transition-transform duration-300
3  motion-reduce:transition-none
4  hover:scale-105 motion-reduce:hover:scale-100
5">
6  <!-- Respects prefers-reduced-motion -->
7</div>

Animation best practices:

1@media (prefers-reduced-motion: reduce) {
2  @theme {
3    --animate-*: initial;
4  }
5}

5.7 Color Contrast #

Tailwind's Default Palette #

Tailwind's default colors are designed with WCAG AA compliance in mind:

Verify custom colors:

1@theme {
2  /* Use oklch for perceptually uniform lightness */
3  --color-text: oklch(0.25 0 0);        /* Dark text */
4  --color-background: oklch(1 0 0);     /* Light background */
5  --color-text-secondary: oklch(0.45 0 0); /* Gray text (AA compliant) */
6}

Tools:

5.8 Semantic HTML + Tailwind #

 1<!-- ❌ Bad - Divs for everything -->
 2<div class="flex items-center gap-4">
 3  <div class="cursor-pointer" onclick="navigate()">Home</div>
 4  <div class="cursor-pointer" onclick="navigate()">About</div>
 5</div>
 6
 7<!-- ✅ Good - Semantic HTML -->
 8<nav class="flex items-center gap-4" aria-label="Main navigation">
 9  <a href="/" class="text-blue-600 hover:underline">Home</a>
10  <a href="/about" class="text-blue-600 hover:underline">About</a>
11</nav>

5.9 Accessible Form Patterns #

 1<div class="space-y-2">
 2  <label for="email" class="block text-sm font-medium">
 3    Email address
 4  </label>
 5  <input
 6    type="email"
 7    id="email"
 8    aria-describedby="email-error"
 9    aria-invalid="true"
10    class="
11      w-full px-3 py-2 border rounded
12      border-gray-300 aria-invalid:border-red-500
13      focus-visible:ring-2 focus-visible:ring-blue-500
14    "
15  />
16  <p id="email-error" class="text-sm text-red-600">
17    Please enter a valid email address
18  </p>
19</div>

5.10 Accessibility Checklist #


6. Integration with Astro and React #

6.1 Astro Integration #

Official Support #

As of Astro 5.2 (January 2025), Tailwind v4 is officially supported via the @tailwindcss/vite plugin.

Setup #

1. Install dependencies:

1npm install tailwindcss @tailwindcss/vite

2. Configure Astro:

1// astro.config.mjs
2import { defineConfig } from "astro/config";
3import tailwindcss from "@tailwindcss/vite";
4
5export default defineConfig({
6  vite: {
7    plugins: [tailwindcss()],
8  },
9});

3. Create CSS file:

1/* src/styles/global.css */
2@import "tailwindcss";
3
4@theme {
5  --color-primary: oklch(0.72 0.11 221.19);
6  --font-body: Inter, sans-serif;
7}

4. Import in layout:

---
// src/layouts/Layout.astro
import '../styles/global.css';
---

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <title>My Site</title>
  </head>
  <body>
    <slot />
  </body>
</html>

Important Notes #

Old @astrojs/tailwind integration:

Astro Component Example #

---
// src/components/Card.astro
export interface Props {
  title: string;
  description: string;
  variant?: 'default' | 'highlighted';
}

const { title, description, variant = 'default' } = Astro.props;

const variantClasses = {
  default: 'bg-white border-gray-200',
  highlighted: 'bg-blue-50 border-blue-300',
};
---

<article class={`
  rounded-xl border p-6 shadow-sm
  ${variantClasses[variant]}
`}>
  <h3 class="text-xl font-semibold mb-2">{title}</h3>
  <p class="text-gray-600">{description}</p>
</article>

Astro + React Components #

1npm install @astrojs/react react react-dom
 1// astro.config.mjs
 2import { defineConfig } from "astro/config";
 3import tailwindcss from "@tailwindcss/vite";
 4import react from "@astrojs/react";
 5
 6export default defineConfig({
 7  integrations: [react()],
 8  vite: {
 9    plugins: [tailwindcss()],
10  },
11});

React component in Astro:

 1// src/components/Counter.tsx
 2import { useState } from 'react';
 3
 4export function Counter() {
 5  const [count, setCount] = useState(0);
 6
 7  return (
 8    <div class="flex items-center gap-4">
 9      <button
10        onClick={() => setCount(count - 1)}
11        class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
12      >
13        -
14      </button>
15      <span class="text-2xl font-bold">{count}</span>
16      <button
17        onClick={() => setCount(count + 1)}
18        class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
19      >
20        +
21      </button>
22    </div>
23  );
24}
---
// src/pages/index.astro
import Layout from '../layouts/Layout.astro';
import { Counter } from '../components/Counter';
---

<Layout>
  <main class="container mx-auto p-8">
    <h1 class="text-4xl font-bold mb-8">Interactive Counter</h1>
    <Counter client:load />
  </main>
</Layout>

6.2 React Integration #

Setup with Vite #

1. Create React app:

1npm create vite@latest my-app -- --template react-ts
2cd my-app

2. Install Tailwind:

1npm install -D tailwindcss @tailwindcss/vite

3. Configure Vite:

1// vite.config.ts
2import { defineConfig } from "vite";
3import react from "@vitejs/plugin-react";
4import tailwindcss from "@tailwindcss/vite";
5
6export default defineConfig({
7  plugins: [react(), tailwindcss()],
8});

4. Setup CSS:

1/* src/index.css */
2@import "tailwindcss";
3
4@theme {
5  --color-primary: oklch(0.55 0.25 262);
6  --color-secondary: oklch(0.75 0.15 195);
7}

5. Import in main:

 1// src/main.tsx
 2import React from 'react';
 3import ReactDOM from 'react-dom/client';
 4import App from './App';
 5import './index.css';
 6
 7ReactDOM.createRoot(document.getElementById('root')!).render(
 8  <React.StrictMode>
 9    <App />
10  </React.StrictMode>,
11);

Reusable Component Patterns #

Button Component:

 1// src/components/Button.tsx
 2import { forwardRef, type ButtonHTMLAttributes } from 'react';
 3
 4interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
 5  variant?: 'primary' | 'secondary' | 'outline';
 6  size?: 'sm' | 'md' | 'lg';
 7}
 8
 9export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
10  ({ variant = 'primary', size = 'md', className = '', ...props }, ref) => {
11    const baseClasses = 'inline-flex items-center justify-center rounded-lg font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed';
12
13    const variantClasses = {
14      primary: 'bg-primary text-white hover:bg-primary-600 focus-visible:ring-primary',
15      secondary: 'bg-secondary text-white hover:bg-secondary-600 focus-visible:ring-secondary',
16      outline: 'border-2 border-primary text-primary hover:bg-primary hover:text-white focus-visible:ring-primary',
17    };
18
19    const sizeClasses = {
20      sm: 'px-3 py-1.5 text-sm',
21      md: 'px-4 py-2 text-base',
22      lg: 'px-6 py-3 text-lg',
23    };
24
25    return (
26      <button
27        ref={ref}
28        className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
29        {...props}
30      />
31    );
32  }
33);
34
35Button.displayName = 'Button';

Usage:

1<Button variant="primary" size="lg" onClick={handleClick}>
2  Click me
3</Button>

Class Name Management #

Using clsx for conditional classes:

1npm install clsx
 1import clsx from 'clsx';
 2
 3interface CardProps {
 4  title: string;
 5  featured?: boolean;
 6  className?: string;
 7}
 8
 9export function Card({ title, featured, className }: CardProps) {
10  return (
11    <div className={clsx(
12      'rounded-xl p-6 shadow-sm',
13      featured ? 'bg-blue-50 border-2 border-blue-500' : 'bg-white border border-gray-200',
14      className
15    )}>
16      <h3 class="text-xl font-semibold">{title}</h3>
17    </div>
18  );
19}

Using tailwind-merge to handle conflicts:

1npm install tailwind-merge
 1import { twMerge } from 'tailwind-merge';
 2
 3export function Card({ className, ...props }: CardProps) {
 4  return (
 5    <div className={twMerge(
 6      'rounded-xl p-6 bg-white',
 7      className  // User's classes override defaults
 8    )}>
 9      {/* ... */}
10    </div>
11  );
12}

Best of both worlds - cn helper:

1// lib/utils.ts
2import { clsx, type ClassValue } from 'clsx';
3import { twMerge } from 'tailwind-merge';
4
5export function cn(...inputs: ClassValue[]) {
6  return twMerge(clsx(inputs));
7}
 1import { cn } from '@/lib/utils';
 2
 3export function Card({ featured, className }: CardProps) {
 4  return (
 5    <div className={cn(
 6      'rounded-xl p-6',
 7      featured && 'bg-blue-50 border-blue-500',
 8      className
 9    )}>
10      {/* ... */}
11    </div>
12  );
13}

TypeScript + Tailwind Best Practices #

Type-safe variant props:

 1import { type VariantProps } from 'class-variance-authority';
 2import { cva } from 'class-variance-authority';
 3
 4const buttonVariants = cva(
 5  'inline-flex items-center justify-center rounded-lg font-medium transition-colors',
 6  {
 7    variants: {
 8      variant: {
 9        primary: 'bg-primary text-white hover:bg-primary-600',
10        secondary: 'bg-secondary text-white hover:bg-secondary-600',
11        outline: 'border-2 border-primary text-primary hover:bg-primary hover:text-white',
12      },
13      size: {
14        sm: 'px-3 py-1.5 text-sm',
15        md: 'px-4 py-2 text-base',
16        lg: 'px-6 py-3 text-lg',
17      },
18    },
19    defaultVariants: {
20      variant: 'primary',
21      size: 'md',
22    },
23  }
24);
25
26interface ButtonProps
27  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
28    VariantProps<typeof buttonVariants> {}
29
30export function Button({ variant, size, className, ...props }: ButtonProps) {
31  return (
32    <button
33      className={cn(buttonVariants({ variant, size }), className)}
34      {...props}
35    />
36  );
37}

6.3 Framework-Agnostic Best Practices #

Component Prop Patterns #

Avoid passing raw classNames:

1// ❌ Anti-pattern
2<Button className="bg-red-500 text-white px-4 py-2" />
3
4// ✅ Better
5<Button variant="danger" size="md" />

When to allow className overrides:

1// Layout components that need flexibility
2<Container className="max-w-screen-2xl">
3
4// Utility wrappers
5<Stack spacing="4" className="md:flex-row">

Composition Over Configuration #

 1// ✅ Flexible composition
 2<Card>
 3  <CardHeader>
 4    <CardTitle>Title</CardTitle>
 5  </CardHeader>
 6  <CardContent>
 7    Content
 8  </CardContent>
 9  <CardFooter>
10    <Button>Action</Button>
11  </CardFooter>
12</Card>
13
14// ❌ Rigid configuration
15<Card
16  title="Title"
17  content="Content"
18  footerButton="Action"
19/>

7. Recommendations and Action Items #

7.1 For New Projects #

  1. Start with Tailwind v4 - No reason to use v3 for greenfield projects
  2. Use the Vite plugin - Best performance and developer experience
  3. Define @theme early - Establish design tokens before building components
  4. Prefer framework components - Minimize @apply usage
  5. Setup Prettier plugin - Automatic class sorting from day one
  6. Plan dark mode strategy - Use design token approach for scalability

7.2 For Existing Projects #

  1. Test migration in a branch - Use npx @tailwindcss/upgrade
  2. Verify browser support - Ensure Safari 16.4+, Chrome 111+, Firefox 128+
  3. Audit custom utilities - Convert @layer utilities to @utility
  4. Update CI/CD - New CLI package and build commands
  5. Train team - New @theme syntax and conventions
  6. Update documentation - Revise style guides and component libraries

7.3 Performance Optimization Checklist #

7.4 Accessibility Checklist #

7.5 Team Collaboration #

  1. Establish conventions - Document when to use @apply vs components
  2. Code review guidelines - Ensure accessibility and performance standards
  3. Component library - Build reusable patterns with clear APIs
  4. Design system documentation - Living style guide with examples
  5. Linting and formatting - Enforce consistency automatically
  6. Regular audits - Review bundle size, accessibility, performance

8. Conclusion #

Tailwind CSS v4 represents a mature, thoughtful evolution of the utility-first approach. The shift to CSS-first configuration via @theme aligns with modern web standards while dramatically improving performance. For teams building with Astro, React, or other modern frameworks, v4 offers:

The migration path from v3 is well-supported with automated tooling, and the framework continues to prioritize developer experience without compromising on performance or capabilities.

For organizations considering Tailwind v4, the benefits clearly outweigh the migration costs, especially for projects that can meet the modern browser requirements.


9. Sources #

Official Documentation #

Migration Guides #

Features and Updates #

Best Practices #

Theme and Customization #

Performance #

Accessibility #

Framework Integration #

Component Patterns #


Report Generated: November 30, 2025 Tailwind CSS Version: v4.0 (stable) Last Updated: January 2025

last updated: