Tailwind v4 uses CSS-first configuration — all config lives in your CSS file (@theme replaces tailwind.config.js)
The @theme directive defines design tokens as CSS custom properties — accessible to both Tailwind and vanilla CSS
Class sorting and grouping prevent style chaos — use prettier-plugin-tailwindcss in CI
Extract components early — repeated class patterns become reusable components, not @apply utilities
shadcn/ui provides the component layer — do not restyle its primitives, override via CSS variables
Biggest mistake: using @apply everywhere — it defeats Tailwind's utility-first approach and bloats CSS output
Plain-English First
Tailwind CSS gives you small building blocks (utilities) instead of pre-built styles. In a large project, the challenge is not writing the utilities — it is organizing them so the codebase stays maintainable as it grows from 10 components to 500. The patterns in this guide prevent the common failure: a codebase where every developer styles things differently, class lists are 200 characters long, and nobody can find the source of a visual bug.
Tailwind CSS scales well when the team follows consistent patterns. The problems in large projects are not technical limitations — they are organizational: inconsistent class ordering, duplicated utility patterns, opaque design tokens, and components that override each other's styles in unpredictable ways.
Tailwind v4 (alpha as of early 2026) changes the configuration model. The tailwind.config.js file is replaced by CSS-first configuration using @theme, @custom-variant, and @utility directives. This moves design tokens into CSS custom properties, making them accessible to both Tailwind utilities and vanilla CSS. Combined with shadcn/ui's CSS variable-based theming, this creates a design system that is both flexible and constrained.
Note: Tailwind v4 features shown here are based on the current alpha release. The API may change before stable release. For production today, use Tailwind v3 with the patterns shown (component extraction, cn(), etc.).
This guide covers the patterns that work at scale: CSS-first configuration, component extraction strategies, class organization, performance optimization, and the shadcn/ui integration that prevents the most common styling conflicts.
Tailwind v4: CSS-First Configuration
Tailwind v4 eliminates tailwind.config.js. All configuration moves into CSS using directives: @theme for design tokens, @custom-variant for custom variants, @utility for reusable utility patterns, and @source for content paths. This is a fundamental shift — design tokens become CSS custom properties that are accessible to both Tailwind utilities and vanilla CSS.
The @theme directive defines your design system: colors, spacing, typography, breakpoints, and animations. Each @theme value becomes a CSS custom property (--color-primary, --spacing-md) and a corresponding Tailwind utility (bg-primary, p-md). This eliminates the gap between Tailwind's theme config and CSS custom properties — they are the same thing.
The migration from v3 to v4 is straightforward for most projects: move colors and spacing from tailwind.config.js to @theme, replace the content array with @source globs, and convert any plugins to @utility or @custom-variant directives.
Design tokens are CSS custom properties — no gap between Tailwind config and CSS variables.
Migrate from v3: move config to @theme, replace content array with @source, convert plugins to @utility.
Component Extraction: The Right Way to Reuse Styles
The correct way to reuse Tailwind patterns is component extraction — not @apply. When a combination of utilities appears in 3 or more places, extract it as a React component with the class string directly on the element. This preserves Tailwind's tree-shaking, keeps styles co-located with their markup, and avoids the cognitive overhead of custom CSS classes.
@Apply creates an opaque abstraction layer. Developers must learn which custom classes exist, what utilities they contain, and where they are defined. Tailwind's content scanner cannot trace @Apply usage reliably, which leads to bloated CSS bundles. Component extraction avoids all of these problems — the styles are visible on the element, the component is importable, and Tailwind can tree-shake correctly.
Component extraction preserves tree-shaking — styles are visible, co-located, and importable.
Rule: 3+ occurrences = component extraction, not @Apply. Use cva for variants.
Key Takeaway
Extract patterns as React components, not @Apply classes — preserves tree-shaking and visibility.
cva (class-variance-authority) handles component variants — type-safe and composable.
cn() from tailwind-merge resolves class conflicts — later classes override earlier ones correctly.
Class Organization: Sorting, Grouping, and Readability
In large projects, class lists grow long and inconsistent. Without a sorting convention, developers order classes differently — some group by function (layout, typography, color), others alphabetically, others arbitrarily. This creates cognitive overhead during code review and can cause CSS specificity conflicts when the same utility appears in different positions across components.
prettier-plugin-tailwindcss solves the ordering problem by auto-sorting classes in a deterministic order. Install it as a dev dependency, add it to your Prettier config, and enforce it in CI with prettier --check. The plugin sorts classes by category: layout, spacing, sizing, typography, visual effects, then interactive states.
For long class lists (20+ classes), group them with comments to improve readability. Each group represents a styling concern: layout, spacing, typography, visual, responsive overrides, dark mode, and conditional states. The cn() utility from tailwind-merge resolves class conflicts and filters falsy values, making conditional styling safe and predictable.
Long class lists: break into groups with comments — layout, spacing, visual, responsive, dark mode
Use cn() for conditional classes — it resolves conflicts and handles falsy values
Tailwind IntelliSense needs classRegex config for cn(), cva(), clsx() — add to .vscode/settings.json
Production Insight
prettier-plugin-tailwindcss enforces deterministic class ordering — prevent style conflicts across the team.
Long class lists should be grouped by function with comments — layout, spacing, visual, responsive.
Rule: enforce class sorting in CI — prettier --check prevents unsorted classes from merging.
Key Takeaway
prettier-plugin-tailwindcss auto-sorts classes — enforce in CI, not by convention.
Group long class lists by function: layout, spacing, typography, visual, interactive, responsive.
Tailwind IntelliSense needs classRegex config for cn(), cva(), clsx() — autocomplete in custom functions.
shadcn/ui Integration: Theming Without Conflicts
shadcn/ui provides copy-paste component primitives that use Tailwind utilities and CSS variables for theming. The integration point is CSS variables — shadcn/ui components read from --background, --primary, --muted, etc. defined in your globals.css. To customize the theme, update the CSS variables — do not override the component's Tailwind classes.
The key pattern: shadcn/ui components are designed to be owned by your codebase. They are not installed as a dependency — they are copied into your project. This means you can modify them, but the modification should happen at the component level (changing the component's markup), not by overriding their styles from the outside with !important or higher-specificity selectors.
shadcn/ui CLI (npx shadcn@latest add) adds components with your theme variables pre-configured
Production Insight
shadcn/ui reads theme from CSS variables — update variables to change the theme, not component overrides.
Components are owned by your codebase — modify the component itself, not override from outside.
Rule: customize via CSS variables and cva variants — never use !important or specificity hacks.
Key Takeaway
shadcn/ui theming is CSS-variable driven — update --color-primary in globals.css, not component overrides.
Components are copy-paste — you own them, extend them via cva, do not override from outside.
Compose primitives into feature components — Card + data = StatsCard, Dialog + Form = CreateDialog.
Performance: CSS Bundle Size and Build Optimization
Tailwind generates a large utility set by default. In production, the content scanner removes unused classes — but only if the scanner paths are configured correctly. Misconfigured paths, @apply abuse, and dynamic class construction prevent tree-shaking and bloat the bundle.
The three rules for CSS bundle performance: configure content paths precisely, never construct class names dynamically (Tailwind cannot detect them), and avoid @apply (it prevents tree-shaking). Monitor bundle size in CI — a sudden increase indicates a configuration regression.
io.thecodeforge.tailwind.performance.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// ============================================// Tailwind Performance — Bundle Size Optimization// ============================================// ---- Rule 1: Content paths must be precise ----// Include only files that use Tailwind classes
/* globals.css */
/* @source "../app/**/*.{ts,tsx}";
@source "../components/**/*.{ts,tsx}";
@source "../lib/**/*.{ts,tsx}";
// Do NOT include:// @source "../**/*.{ts,tsx}"; <- Too broad, scans node_modules// @source "../.next/**/*"; <- Build artifacts, includes generated code */// ---- Rule 2: Never construct class names dynamically ----// Tailwind scans for literal class strings — it cannot evaluate expressions// WRONG: Dynamic class construction — Tailwind cannot detect thesefunctiongetBadgeColor(status: string) {
const colors: Record<string, string> = {
active: 'bg-green-500 text-white',
inactive: 'bg-gray-500 text-white',
pending: 'bg-yellow-500 text-black',
}
return colors[status] ?? 'bg-gray-500 text-white'
}
// This works at runtime, but Tailwind cannot detect the classes// during build. They may be purged if not used elsewhere.// CORRECT: Use a lookup object with full class strings// AND list all possible classes in a safelist comment
/* safelist comment — ensures Tailwind includes these classes */
/*
tailwind safelist:
bg-green-500 text-white
bg-gray-500 text-white
bg-yellow-500 text-black
*/
// OR: Use cva for variants (preferred)import { cva } from'class-variance-authority'const badgeVariants = cva(
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',
{
variants: {
status: {
active: 'bg-green-500 text-white',
inactive: 'bg-gray-500 text-white',
pending: 'bg-yellow-500 text-black',
},
},
defaultVariants: {
status: 'active',
},
}
)
// cva outputs full class strings — Tailwind detects them during build// ---- Rule 3: Avoid template literals for class names ----// WRONG: Template literal — Tailwind cannot parse thisconst size = 'md'const className = `p-${size === 'sm' ? '2' : size === 'md' ? '4' : '6'}`
// CORRECT: Map sizes to full class stringsconst sizeClasses = {
sm: 'p-2',
md: 'p-4',
lg: 'p-6',
} asconstconst className2 = sizeClasses[size as keyof typeof sizeClasses]
// ---- Bundle size monitoring in CI ----// File: scripts/check-css-size.sh// #!/bin/bash// # Build CSS and check size// npx tailwindcss --input app/globals.css --output /tmp/tw-output.css --minify// SIZE=$(wc -c < /tmp/tw-output.css)// MAX_SIZE=150000 # 150KB max//// if [ $SIZE -gt $MAX_SIZE ]; then// echo "CSS bundle too large: ${SIZE} bytes (max: ${MAX_SIZE})"// echo "Check for @apply usage or dynamic class construction"// exit 1// fi//// echo "CSS bundle OK: ${SIZE} bytes"// ---- Performance comparison ----
/*
Scenario | CSSSize (minified) | Gzipped
---------------------------|--------------------|--------
CleanTailwind (10 pages) | 12KB | 3KB
With @apply (180 classes) | 450KB | 92KB
Component extraction | 142KB | 22KB
After optimization | 45KB | 8KB
*/
// ---- Tailwind v4: Built-in performance features ----// v4 uses Lightning CSS for faster builds and smaller output
/* Key v4 performance improvements:
- LightningCSS replaces PostCSS — 10x faster builds
- Automatic content detection in v4 — no content array needed for most projects
- Smallerdefault utility set — only generates utilities used in your project
- @utility directives are tree-shakeable — unlike @apply in v3
*/
Dynamic Class Construction Breaks Tree-Shaking
Tailwind scans for literal class strings — template literals and dynamic expressions are invisible
Dynamic class construction (bg-${color}-500) produces classes Tailwind cannot detect during build
Use cva for variants — it outputs full class strings that Tailwind can scan
Safelist comments ensure specific classes are always included — use sparingly
Monitor CSS bundle size in CI — a sudden increase indicates a tree-shaking regression
Production Insight
Tailwind scans literal class strings only — dynamic construction (template literals, expressions) is invisible.
@Apply prevents tree-shaking — the CSS bundle grows linearly with @Apply class count.
Rule: use cva for variants or lookup objects with full strings.
Key Takeaway
Dynamic class construction breaks Tailwind's tree-shaking — use cva or lookup objects with full strings.
@Apply creates opaque abstractions that prevent tree-shaking — extract as components instead.
Monitor CSS bundle size in CI — a sudden increase indicates a configuration regression.
● Production incidentPOST-MORTEMseverity: high
@apply Abuse Generated 450KB CSS Bundle and Broke Tree-Shaking
Symptom
Lighthouse performance score dropped from 92 to 58. The CSS bundle was 450KB (gzipped: 92KB). Page load on 3G took 5.4 seconds. Developers reported that new CSS classes they added were not appearing — Tailwind's content scanner could not trace them through the @apply indirection.
Assumption
@Apply was the correct way to create reusable styles. The team treated @Apply as a way to write custom CSS classes with Tailwind utilities inside, creating a layer of abstraction they believed would improve maintainability.
Root cause
The team created 180 custom @Apply classes (e.g., .card-base, .btn-primary, .input-field) that composed Tailwind utilities. Each @Apply class was used in only 1-3 components. Tailwind's content scanner could not determine which @Apply classes were actually used because they were referenced by class name, not by the utilities they contained. As a result, Tailwind included all 180 @Apply classes and their full utility compositions in the output CSS, even when only 40 were used on the current page. Additionally, the @Apply classes created a second styling layer that developers had to learn on top of Tailwind's utility classes, increasing cognitive load rather than reducing it.
Fix
Replaced all @Apply classes with component extraction — moved the repeated utility patterns into React components with the class strings directly on the elements. For the 12 genuinely reusable patterns (button variants, input styles), created @utility directives in the CSS file which Tailwind v4 can tree-shake correctly. Removed tailwind.config.js and migrated to CSS-first configuration with @theme. CSS bundle dropped from 450KB to 142KB. Lighthouse score recovered to 94.
Key lesson
@Apply creates opaque abstractions that Tailwind cannot tree-shake — use component extraction instead.
If a pattern appears in 3+ places, extract it as a component, not a CSS class.
Tailwind v4 @utility directives are tree-shakeable — use them for genuinely reusable patterns.
Monitor CSS bundle size in CI — it is an early warning sign of @Apply abuse.
Production debug guideDiagnose styling conflicts, bundle size, and configuration issues6 entries
Symptom · 01
CSS class not applying — styles missing on rendered elements
→
Fix
Check the content paths in the Tailwind config — the scanner must include the file where the class is used
Symptom · 02
CSS bundle size exceeds 200KB
→
Fix
Search for @apply usage — each @apply class prevents tree-shaking. Replace with component extraction.
Symptom · 03
Styles differ between dev and production builds
→
Fix
Check for purge/content misconfiguration — dev includes all classes, production strips unused ones
Symptom · 04
shadcn/ui component styles not updating after theme change
→
Fix
Verify CSS variables are defined in :root or the correct selector — shadcn/ui reads from CSS variables, not Tailwind theme values directly
Symptom · 05
Class ordering causes style conflicts
→
Fix
Install prettier-plugin-tailwindcss — it auto-sorts classes in a deterministic order that prevents specificity conflicts
Symptom · 06
Dark mode styles not applying
→
Fix
Check the darkMode strategy — 'class' requires a .dark class on an ancestor, 'media' uses prefers-color-scheme
★ Tailwind CSS Quick Debug ReferenceFast commands for diagnosing Tailwind issues in large projects
Unused classes in production bundle−
Immediate action
Check CSS bundle size and @apply usage
Commands
npx tailwindcss --content './app/**/*.{ts,tsx}' --output /tmp/tw-debug.css 2>&1 | head -5
update variables in globals.css, do not override components
5
Dynamic class construction breaks tree-shaking
use cva or lookup objects with full strings
6
Monitor CSS bundle size in CI
a sudden increase indicates @apply abuse or misconfigured content paths
Common mistakes to avoid
8 patterns
×
Using @apply for reusable patterns instead of component extraction
Symptom
CSS bundle grows to 450KB because Tailwind cannot tree-shake @apply classes. Developers must learn a second abstraction layer on top of Tailwind utilities. New classes sometimes fail to appear because the content scanner cannot trace @apply usage.
Fix
Extract patterns as React components with the class string directly on the element. Use cva (class-variance-authority) for component variants. Reserve @utility for genuinely global patterns (prose styles, scrollbar).
×
Constructing class names dynamically with template literals
Symptom
Classes generated at runtime (bg-${color}-500) work in development but are purged in production. Tailwind's content scanner only detects literal class strings — expressions and template literals are invisible.
Fix
Use lookup objects with full class strings, or use cva for variants. Both output literal strings that Tailwind can scan during build. Add a safelist comment if dynamic construction is unavoidable.
×
Not installing prettier-plugin-tailwindcss
Symptom
Different developers order classes differently. Code reviews become about class ordering instead of logic. CSS specificity conflicts arise from inconsistent ordering — hover states override base states unpredictably.
Fix
Install prettier-plugin-tailwindcss and enforce in CI with prettier --check. The plugin auto-sorts classes in a deterministic order: layout, spacing, sizing, typography, visual, interactive.
×
Overriding shadcn/ui component styles from the outside
Symptom
Specificity wars between shadcn/ui's classes and custom overrides. !important declarations accumulate. Theme changes require updating overrides in multiple files instead of one CSS variable.
Fix
Update CSS variables in globals.css to change the theme. Modify the component's cva definition to add variants. Do not override from the outside — own the component and change it directly.
×
Content paths include too many directories
Symptom
CSS bundle includes classes from test files, storybook stories, and scripts that are never used in production. Build time increases because Tailwind scans more files than necessary.
Fix
Configure @source to include only app/, components/, and lib/ directories. Exclude test directories, storybook, and scripts. In v3, ensure the content array does not include '*/.{ts,tsx}'.
×
Not using cn() for conditional classes
Symptom
Class conflicts when conditional classes override base classes incorrectly. px-4 and px-6 both apply — CSS source order determines which wins, not intent. Undefined values produce 'undefined' in the class string.
Fix
Use cn() from tailwind-merge + clsx. It resolves Tailwind class conflicts (later overrides earlier), handles conditional values (filters out falsy), and merges class strings correctly.
Layouts break on mobile because styles were designed for desktop and adapted down. flex-row on mobile causes horizontal overflow. Fixed widths (w-96) break on narrow screens.
Fix
Use mobile-first approach: unprefixed classes are mobile styles, prefixed classes (sm:, md:, lg:) override at larger breakpoints. flex-col on mobile, sm:flex-row on tablet+.
×
Not configuring Tailwind IntelliSense for custom functions
Symptom
No autocomplete for classes inside cn(), cva(), or clsx() calls. Developers must memorize class names or copy from documentation. Typos in class names are not caught until runtime.
Fix
Add classRegex patterns to .vscode/settings.json for cn(), cva(), clsx(), and twMerge(). This enables IntelliSense autocomplete inside function calls.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
What is the difference between @apply and component extraction in Tailwi...
Q02SENIOR
How does Tailwind v4's CSS-first configuration differ from v3's JavaScri...
Q03SENIOR
How do you integrate shadcn/ui with Tailwind without creating style conf...
Q04SENIOR
Why does dynamic class construction break Tailwind's tree-shaking, and h...
Q05JUNIOR
What is the cn() utility and why is it necessary for Tailwind projects?
Q01 of 05SENIOR
What is the difference between @apply and component extraction in Tailwind, and when should you use each?
ANSWER
@Apply creates a CSS class that composes Tailwind utilities. It appears to offer reuse, but it creates problems at scale: Tailwind cannot reliably tree-shake @apply classes (the content scanner cannot trace class-name references), it creates an opaque abstraction layer that developers must learn, and it bloats the CSS bundle because all @apply compositions are included even when unused.
Component extraction moves the repeated utility pattern into a React component with the class string directly on the element. This preserves Tailwind's tree-shaking (the class strings are literal and scannable), keeps styles visible (no hidden abstractions), and co-locates styles with their markup.
The rule: if a pattern appears in 3+ places, extract it as a component — not a @apply class. Use cva for component variants. Reserve @utility for genuinely global patterns that cannot be components (prose styles, scrollbar).
Q02 of 05SENIOR
How does Tailwind v4's CSS-first configuration differ from v3's JavaScript config?
ANSWER
In v3, configuration lives in tailwind.config.js — a JavaScript file that exports a theme object with colors, spacing, fonts, and plugins. The content array specifies which files to scan. Custom utilities and variants are defined as plugins.
In v4, all configuration moves into CSS using directives. @theme defines design tokens as CSS custom properties — each value becomes both a CSS variable (--color-primary) and a Tailwind utility (bg-primary). @source replaces the content array with glob patterns. @utility replaces simple plugins. @custom-variant replaces variant plugins.
The key advantage: design tokens are CSS custom properties that are accessible to both Tailwind utilities and vanilla CSS. There is no gap between the Tailwind config and CSS — they are the same definitions. Dark mode overrides the same custom properties in a .dark selector.
Q03 of 05SENIOR
How do you integrate shadcn/ui with Tailwind without creating style conflicts?
ANSWER
shadcn/ui components read from CSS variables (--color-primary, --color-background, etc.) defined in globals.css. To customize the theme, update the CSS variables — do not override the component's Tailwind classes from the outside.
shadcn/ui components are copy-paste — they live in your codebase, not as a dependency. To add custom variants, modify the component's cva definition directly. Do not use !important or higher-specificity selectors to override shadcn/ui styles from external CSS.
The integration pattern: globals.css defines CSS variables in @theme, shadcn/ui components consume those variables via Tailwind utilities (bg-primary, text-foreground), and you extend components by adding variants to the cva definition — not by overriding from the outside.
Q04 of 05SENIOR
Why does dynamic class construction break Tailwind's tree-shaking, and how do you work around it?
ANSWER
Tailwind's content scanner looks for literal class strings in your source files. It uses regex matching to find strings that look like Tailwind classes. Dynamic construction — template literals like bg-${color}-500, computed expressions, or object lookups with interpolated values — produces class strings at runtime that the scanner cannot detect during build.
The workaround depends on the use case:
1. Use cva for component variants — it outputs full literal class strings that the scanner can detect.
2. Use a lookup object with full class strings as values — map 'active' to 'bg-green-500 text-white'.
3. Add a safelist comment listing all possible classes — forces Tailwind to include them.
4. In Tailwind v4, use @utility for genuinely reusable patterns — these are tree-shakeable.
Q05 of 05JUNIOR
What is the cn() utility and why is it necessary for Tailwind projects?
ANSWER
cn() combines clsx and tailwind-merge. clsx handles conditional classes — it filters out falsy values and joins strings. tailwind-merge resolves Tailwind class conflicts — when two utilities conflict (px-4 and px-6), it keeps only the later one.
Without cn(), conditional classes produce conflicts. For example, cn('px-4', isActive && 'px-6') produces 'px-6' when active (correct). But using string concatenation: 'px-4 ' + (isActive ? 'px-6' : '') produces 'px-4 px-6' — both classes applied, and CSS source order determines which wins, not intent.
cn() also handles undefined and false values — they are filtered out, preventing 'undefined' from appearing in the class string. This is essential for conditional styling patterns common in component libraries.
01
What is the difference between @apply and component extraction in Tailwind, and when should you use each?
SENIOR
02
How does Tailwind v4's CSS-first configuration differ from v3's JavaScript config?
SENIOR
03
How do you integrate shadcn/ui with Tailwind without creating style conflicts?
SENIOR
04
Why does dynamic class construction break Tailwind's tree-shaking, and how do you work around it?
SENIOR
05
What is the cn() utility and why is it necessary for Tailwind projects?
JUNIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
Should I use Tailwind CSS or CSS Modules for a large project?
Tailwind CSS is better for large projects because it enforces consistency through a constrained utility set. CSS Modules allow any CSS, which leads to divergent styling patterns across teams. Tailwind's utility-first approach, combined with prettier-plugin-tailwindcss for sorting and cva for variants, creates a more maintainable codebase at scale. CSS Modules are better for isolated, highly custom components where Tailwind's utility vocabulary is insufficient.
Was this helpful?
02
How do I handle styles that Tailwind utilities cannot express?
Use @utility in Tailwind v4 for complex CSS that cannot be expressed as utilities (prose styles, complex animations, custom scrollbars). For component-specific styles, use inline style attributes or a scoped CSS file. Avoid creating a global CSS file with arbitrary selectors — this defeats Tailwind's scoping model.
Was this helpful?
03
Can I use Tailwind with a design system that has strict spacing and color scales?
Yes. Define your design system's tokens in @theme — replace Tailwind's default spacing and color scales with your own. This ensures all components use the approved tokens. Developers cannot use arbitrary values (p-[13px]) unless explicitly enabled. The constrained utility set enforces design system compliance.
Was this helpful?
04
How do I migrate from Tailwind v3 to v4?
Three steps: 1) Move colors, spacing, and fonts from tailwind.config.js to @theme in globals.css. 2) Replace the content array with @source globs. 3) Convert plugins to @utility or @custom-variant directives. Run the Tailwind v4 codemod (npx @tailwindcss/upgrade) for automated migration. Test thoroughly — some utility names and defaults changed.
Was this helpful?
05
How do I test Tailwind-styled components?
Tailwind-styled components can be tested with any testing framework (Vitest, Jest, React Testing Library) because Tailwind compiles to regular CSS classes. Three approaches:
Snapshot tests — capture the rendered HTML with class names. Fragile because any class change breaks the snapshot.
Visual regression tests — capture screenshots and compare pixel-by-pixel. Tools like Playwright, Percy, or Chromatic. Best for catching unintended visual changes.
Class assertion tests — assert that specific Tailwind classes are present on elements. Use getByRole or getByText to find elements, then check className for expected classes.
For unit testing, ensure Tailwind is compiled before tests run. In Vitest, use the @tailwindcss/vite plugin or configure PostCSS in the test environment.