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 #
- March 6, 2024: v4-alpha launched and open-sourced
- November 21, 2024: v4-beta-1 released
- January 22, 2025: Stable v4.0 released
1.2 Major New Features #
High-Performance Engine (Oxide) #
Tailwind v4 features a complete rewrite with critical components in Rust, delivering:
- 3.78x faster full builds (378ms → 100ms on Tailwind website)
- 8.8x faster incremental builds with new CSS (44ms → 5ms)
- 182x faster incremental builds with no new CSS (35ms → 192µs)
- 35% smaller installed package size
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:
- Configuration lives where it's used
- No JavaScript context switching
- All design tokens become CSS variables automatically
- Runtime access without JavaScript overhead
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:
- Respects
.gitignoreto avoid scanning dependencies - Automatically ignores binary extensions (images, videos, zip files)
- No content array configuration required
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 #
inset-shadow-*andinset-ring-*for layered shadowsfield-sizingfor auto-resizing textareascolor-schemefor scrollbar styling in dark modefont-stretchfor variable font controlinertvariant for non-interactive elementsnth-*variants for advanced selectionin-*variant (likegroup-*without class requirement)
Modernized Color Palette #
Colors upgraded from rgb to oklch color space for:
- Wider gamut support
- More vivid colors on modern displays
- Better perceptual uniformity
- P3 color space compatibility
1.3 Breaking Changes #
Browser Requirements #
Minimum versions:
- Safari 16.4+
- Chrome 111+
- Firefox 128+
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 #
- Border and ring utilities now default to
currentColorinstead of specific gray values - Placeholder color now at 50% opacity of current text color
- Explicitly specify colors where the old defaults were relied upon
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 #
- Individual transform properties (
rotate,scale,translate) replace combinedtransformusage - Variant stacking order reversed (left-to-right instead of right-to-left)
- JavaScript config files require explicit
@configdirectives resolveConfigfunction removed; use CSS variables directly- Sass/Less/Stylus preprocessing no longer supported
1.4 Migration Strategy #
Automated Migration Tool #
The official upgrade tool automates most migration work:
1npx @tailwindcss/upgrade
Requirements:
- Node.js 20 or higher
- Clean Git working directory (use
--forceto bypass)
What it does:
- Updates dependencies
- Migrates
tailwind.config.jsto@themein CSS - Updates template files with new syntax
- Handles utility name changes
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:
- Verify visual appearance across all pages
- Test interactive states (hover, focus, active)
- Check dark mode if implemented
- Validate responsive breakpoints
2. Theme Customization with @theme Directive #
2.1 Understanding @theme #
The @theme directive defines special CSS variables that:
- Create corresponding utility classes
- Become CSS custom properties accessible anywhere
- 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:
- CSS variables:
var(--color-mint-500),var(--font-script),var(--spacing-huge) - Utility classes:
bg-mint-500,text-mint-500,font-script,p-huge,m-huge
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');
Design Token Approach (Recommended) #
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:
- Write
bg-backgroundinstead ofbg-white dark:bg-gray-950 - Changes propagate automatically
- Reduces template complexity
- More maintainable at scale
2.8 When to Use @theme vs :root #
Use @theme when:
- You want a utility class generated
- The value is part of your design system
- You need theme tokens accessible across utilities
Use :root when:
- The variable is for internal component use only
- You don't need corresponding utilities
- You want a simple CSS variable
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 #
3.1 Recommended File Structure #
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):
theme- Design tokens and theme variablesbase- Reset styles and base element stylescomponents- Component patternsutilities- 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:
- Very small, highly reusable elements (buttons, badges, inputs)
- Base component styles that don't need programmatic variation
- Vendor library customization where templates aren't accessible
Poor use cases:
- Complex components with many states
- Anything easily represented as a React/Vue/Astro component
- One-off styling patterns
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:
- All available design tokens
- Component examples
- Accessibility guidelines
- Usage patterns
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:
- Complex pseudo-elements or pseudo-classes
- Advanced selectors (
:has(),:nth-child()patterns) - Component-specific animations
- Styles that would be unwieldy with utilities
4. Performance Considerations #
4.1 Build Performance #
Tailwind v4 Benchmarks #
Tailwind CSS website:
- v3: 960ms full build
- v4: 105ms full build
- 9.14x faster
Catalyst UI kit:
- v3: 341ms full build
- v4: 55ms full build
- 6.2x faster
Incremental builds:
- No new CSS: 182x faster (35ms → 192µs)
- With new CSS: 8.8x faster (44ms → 5ms)
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:
- Near-instantaneous browser updates in dev mode
- Minimal context switching
- Better flow state
Automatic Content Detection Benefits #
- No manual configuration updates when adding new files
- Faster onboarding for new developers
- Fewer build configuration errors
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:
aria-checked:*- For checkboxes, switchesaria-disabled:*- For disabled statesaria-expanded:*- For accordions, dropdownsaria-hidden:*- For visibility controlaria-pressed:*- For toggle buttonsaria-selected:*- For tabs, listboxesaria-invalid:*- For form validation
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:
- Always provide focus indicators
- Use
focus-visible:*instead offocus:*to avoid visual clutter on click - Ensure 3:1 contrast ratio for focus indicators
- Minimum 2px focus outline
Skip Links #
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:
- Always provide
motion-reduce:*alternatives - Disable autoplay for users preferring reduced motion
- Use
transition-discretefor enter/exit animations
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:
- Gray 900 on white: ✅ AAA (>7:1)
- Gray 700 on white: ✅ AA (>4.5:1)
- Gray 600 on white: ⚠️ Borderline
- Gray 500 on white: ❌ Fails
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 #
- ✅ All interactive elements are keyboard accessible
- ✅ Focus indicators have 3:1 contrast ratio
- ✅ Text has 4.5:1 contrast (AA) or 7:1 (AAA)
- ✅ ARIA attributes are accurate and up-to-date
- ✅ Screen reader text provided where visual context exists
- ✅ Animations respect
prefers-reduced-motion - ✅ Forms have associated labels and error messages
- ✅ Semantic HTML used where possible
- ✅ Images have
altattributes - ✅ Headings follow logical hierarchy (h1 → h2 → h3)
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:
- Deprecated for Tailwind v4
- Uninstall and use
@tailwindcss/vitedirectly - Migration guide: https://docs.astro.build/en/guides/integrations-guide/tailwind/
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 #
- Start with Tailwind v4 - No reason to use v3 for greenfield projects
- Use the Vite plugin - Best performance and developer experience
- Define @theme early - Establish design tokens before building components
- Prefer framework components - Minimize
@applyusage - Setup Prettier plugin - Automatic class sorting from day one
- Plan dark mode strategy - Use design token approach for scalability
7.2 For Existing Projects #
- Test migration in a branch - Use
npx @tailwindcss/upgrade - Verify browser support - Ensure Safari 16.4+, Chrome 111+, Firefox 128+
- Audit custom utilities - Convert
@layer utilitiesto@utility - Update CI/CD - New CLI package and build commands
- Train team - New
@themesyntax and conventions - Update documentation - Revise style guides and component libraries
7.3 Performance Optimization Checklist #
- ✅ Use
@tailwindcss/viteplugin for Vite projects - ✅ Avoid dynamic class construction
- ✅ Minimize custom utilities and
@applyusage - ✅ Leverage automatic content detection
- ✅ Implement design token approach for theming
- ✅ Use native CSS features (cascade layers,
@property,color-mix()) - ✅ Setup proper caching headers for CSS files
- ✅ Consider critical CSS inlining for above-fold content
7.4 Accessibility Checklist #
- ✅ Use ARIA variants (
aria-checked:*,aria-disabled:*, etc.) - ✅ Implement visible focus indicators with
focus-visible:* - ✅ Provide screen reader text with
sr-only - ✅ Respect motion preferences with
motion-reduce:* - ✅ Ensure 4.5:1 text contrast minimum
- ✅ Use semantic HTML
- ✅ Test with keyboard navigation
- ✅ Validate with automated tools (axe, Lighthouse)
7.5 Team Collaboration #
- Establish conventions - Document when to use
@applyvs components - Code review guidelines - Ensure accessibility and performance standards
- Component library - Build reusable patterns with clear APIs
- Design system documentation - Living style guide with examples
- Linting and formatting - Enforce consistency automatically
- 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:
- 10x faster builds through the Oxide engine
- Simpler setup with zero configuration by default
- Better theming via native CSS variables
- Modern CSS features like cascade layers and
@property - Improved accessibility with ARIA variants
- Excellent framework integration via first-party plugins
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 #
- Tailwind CSS v4.0
- Upgrade guide - Tailwind CSS
- Theme variables - Tailwind CSS
- Dark mode - Tailwind CSS
- Reusing Styles - Tailwind CSS
- astrojs/tailwind - Astro Docs
- Astro 5.2
Migration Guides #
- Migration from v3 to v4 | DeepWiki
- What's New and Migration Guide: Tailwind CSS v4.0
- Real-World Migration Steps from Tailwind CSS v3 to v4
Features and Updates #
- Tailwind CSS v4 Is Here: All the Updates You Need to Know
- What's New in Tailwind CSS 4.0 – Full Feature Breakdown
- Everything you need to know about Tailwind CSS v4
Best Practices #
- Tailwind CSS 4 Best Practices for Enterprise-Scale Projects (2025 Playbook)
- How to Master Tailwind CSS: Best Practices 2025
- Best Practices for Using Tailwind CSS in Large Projects
- Tailwind CSS Best Practices for Enterprise Projects
- Best Practices for Writing Scalable Tailwind CSS Code
Theme and Customization #
- How to use custom color themes in TailwindCSS v4
- Goodbye tailwind.config.js? TailwindCSS v4's New CSS-First Setup
- A First Look at Setting Up Tailwind CSS v4.0
- Build a Flawless, Multi-Theme System using New Tailwind CSS v4 & React
- Dark Mode with Design Tokens in Tailwind CSS
Performance #
- Tailwind CSS v4.0: 40% Faster Builds & Performance Guide
- Tailwind CSS v4: What's New and Why It Matters for Developers in 2025
- Optimizing for Production - Tailwind CSS
Accessibility #
- Tying Tailwind styling to ARIA attributes
- Creating an Accessible Toggle Switch in Tailwind CSS
- Accessibility Beyond Compliance: Real Patterns for React and Tailwind
- Building Accessible UI with Tailwind CSS and ARIA
- ARIA Integration | Tailwind
Framework Integration #
- How to Use Tailwind CSS v4 in Astro
- How to setup Tailwind CSS v4.1.5 with Vite + React (2025 updated guide)
- Install Tailwind CSS with Vite (v4 Plugin Guide)
- Setting Up Tailwind CSS with Vite: A Quick and Easy Guide
Component Patterns #
- Building reusable React components using Tailwind CSS
- Component Abstraction: Writing Reusable UI with Tailwind + React
- Tailwind Best Practices: Structuring Utility Classes, Creating Reusable Components
- Building Reusable React Components Using Tailwind
Report Generated: November 30, 2025 Tailwind CSS Version: v4.0 (stable) Last Updated: January 2025