Fix: Tailwind CSS Classes Not Applying — Styles Missing in Production or Development
Part of: React & Frontend Errors
Quick Answer
How to fix Tailwind CSS not applying styles — content config paths, JIT mode, dynamic class names, PostCSS setup, CDN vs build tool, and purging issues.
The Problem
Tailwind CSS classes are in the HTML but styles aren’t applied:
<!-- Classes written correctly but have no effect -->
<div class="bg-blue-500 text-white p-4 rounded-lg">
Hello World
</div>
<!-- Renders as unstyled text -->Or styles work in development but disappear in the production build:
npm run build # Production build
# All custom Tailwind classes removed — only base styles remainOr dynamically constructed class names don’t work:
// Dynamic class name — doesn't work
const color = 'blue';
const size = '500';
const className = `bg-${color}-${size}`; // bg-blue-500 — NOT generatedOr after upgrading from Tailwind v2 to v3, many classes stopped working.
Why This Happens
Tailwind CSS uses a purge/scan step to generate only the CSS classes actually used in your codebase. This keeps bundle sizes small but causes classes to disappear if the scanner can’t find them. The scanner operates at build time by reading your source files as plain text and matching complete class name strings against a regex. It does not evaluate JavaScript, compile templates, or resolve variables. Any class name that does not appear as a complete, unbroken string in one of the scanned files is excluded from the output CSS.
The exact configuration required depends on your Tailwind version. Tailwind v3 uses a content array in tailwind.config.js and relies on PostCSS. Tailwind v4 introduced a CSS-first configuration model that replaces the JavaScript config file with @import "tailwindcss" and CSS-based @theme blocks. Mixing v3 and v4 configuration patterns in the same project is the most common cause of “nothing works at all” after an upgrade.
The build tooling layer adds another variable. PostCSS is the default pipeline for v3, but v4 ships with Lightning CSS as its default engine in some setups. Framework-specific integrations (Next.js, Vite, Astro, Laravel) each have their own content path conventions and plugin wiring that can conflict with a manually configured tailwind.config.js.
Fix 1: Configure content Paths Correctly
The content array tells Tailwind where to look for class names. Every file type that contains Tailwind classes must be included:
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
// React / Next.js
'./src/**/*.{js,jsx,ts,tsx}',
'./pages/**/*.{js,jsx,ts,tsx}',
'./components/**/*.{js,jsx,ts,tsx}',
'./app/**/*.{js,jsx,ts,tsx}',
// Vue
'./src/**/*.{vue,js,ts}',
// HTML files
'./**/*.html',
'!./node_modules/**', // Exclude node_modules
// If classes come from external libraries
'./node_modules/your-component-lib/**/*.{js,ts}',
],
theme: {
extend: {},
},
plugins: [],
};Framework-specific content paths:
// Next.js (App Router + Pages Router)
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
'./src/**/*.{js,ts,jsx,tsx}', // If using src directory
],
// Astro
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
// Laravel (Blade templates)
content: [
'./resources/**/*.blade.php',
'./resources/**/*.js',
'./resources/**/*.vue',
],
// Vite + React
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],Common content path mistakes:
// WRONG — missing file extensions
content: ['./src/**/*'] // Matches all files but Tailwind needs to know the type
// WRONG — path doesn't match project structure
content: ['./pages/**/*.tsx'] // But files are in ./src/pages/ — nothing found
// CORRECT — explicit extensions, correct paths
content: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html']Fix 2: Tailwind v3 vs v4 Configuration
Tailwind v4 changed the configuration model entirely. Mixing v3 and v4 patterns is the most common cause of a completely blank output:
/* Tailwind v3 — three directives in your CSS entry file */
@tailwind base;
@tailwind components;
@tailwind utilities;/* Tailwind v4 — single import, CSS-first config */
@import "tailwindcss";
/* Theme customization in CSS, not tailwind.config.js */
@theme {
--color-primary: oklch(70% 0.2 240);
--font-display: "Inter", sans-serif;
}Key differences between v3 and v4:
// v3 — JavaScript config file (tailwind.config.js)
module.exports = {
content: ['./src/**/*.{js,jsx}'],
theme: {
extend: {
colors: {
primary: '#3b82f6',
},
},
},
plugins: [require('@tailwindcss/forms')],
};/* v4 — CSS-first config (no tailwind.config.js needed) */
@import "tailwindcss";
@plugin "@tailwindcss/forms";
@theme {
--color-primary: #3b82f6;
}v4 content detection is automatic. Tailwind v4 scans your entire project by default using heuristics (it reads files referenced in your HTML/JS imports). You do not need a content array. If automatic detection misses files, add explicit sources:
/* v4 — explicitly add sources */
@import "tailwindcss";
@source "../components/**/*.tsx";
@source "../node_modules/my-ui-lib/dist/**/*.js";Import the CSS in your entry point:
// src/main.js or src/index.js (React/Vue)
import './index.css'; // Must import the CSS file with @tailwind directives
// src/app/layout.tsx (Next.js App Router)
import './globals.css';Fix 3: Fix Dynamic Class Names
Tailwind can only generate classes that appear as complete strings in scanned files:
// WRONG — template literal — Tailwind can't evaluate at scan time
const colorClass = `bg-${color}-500`; // bg-blue-500 NOT generated
// WRONG — string concatenation
const cls = 'bg-' + color + '-500'; // NOT generated
// WRONG — computed object key
const classes = { [`text-${size}`]: true }; // NOT generated
// CORRECT — complete class names must exist as strings somewhere
// Option 1 — use a mapping object with complete class names
const colorMap = {
blue: 'bg-blue-500',
red: 'bg-red-500',
green: 'bg-green-500',
};
const cls = colorMap[color]; // 'bg-blue-500' exists as a string — generated
// Option 2 — conditional with complete class names
const cls = color === 'blue' ? 'bg-blue-500' : 'bg-red-500';
// Option 3 — safelist in tailwind.config.js
// For class names you can't write as complete strings// tailwind.config.js — safelist specific classes or patterns
module.exports = {
content: ['./src/**/*.{js,jsx}'],
safelist: [
'bg-blue-500',
'bg-red-500',
'bg-green-500',
// Pattern-based safelist
{
pattern: /bg-(red|blue|green)-(400|500|600)/,
variants: ['hover', 'dark'], // Include hover and dark mode variants
},
],
};Common Mistake: Many developers discover their dynamic classes “work in development but break in production.” This is because the development server may use a different purging strategy (or no purging). In production, only classes found in the content scan are generated.
Fix 4: PostCSS vs Lightning CSS
Tailwind v3 requires PostCSS. Tailwind v4 can use either PostCSS or Lightning CSS (its new default in some setups). Mixing the two causes silent failures:
// PostCSS config for Tailwind v3
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};# Verify packages are installed (v3)
npm list tailwindcss postcss autoprefixer
# If missing:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p # Creates tailwind.config.js and postcss.config.jsTailwind v4 with Vite uses the Vite plugin, not PostCSS:
// vite.config.ts — Tailwind v4
import tailwindcss from '@tailwindcss/vite';
export default {
plugins: [tailwindcss()],
};
// No postcss.config.js needed — the Vite plugin replaces itTailwind v4 with PostCSS (non-Vite projects):
// postcss.config.js — Tailwind v4 PostCSS plugin
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
},
};
// Note: the plugin name changed from 'tailwindcss' to '@tailwindcss/postcss'Turbopack (Next.js) vs Webpack — JIT behavior differences:
// next.config.js — Turbopack uses its own CSS pipeline
/** @type {import('next').NextConfig} */
const nextConfig = {
// Turbopack (Next.js 14+) handles Tailwind natively
// No PostCSS config changes needed
// But: Turbopack does NOT read postcss.config.js plugins
// beyond tailwindcss and autoprefixer
};If you use custom PostCSS plugins alongside Tailwind in a Next.js project with Turbopack enabled, those plugins are ignored. Switch back to Webpack (next dev without --turbopack) or move the custom PostCSS logic elsewhere.
Fix 5: Docker Build and Storybook Integration
Tailwind classes can disappear in Docker builds or Storybook when the content scanner runs in a different file system context:
Docker — content paths resolve relative to the build context:
# WRONG — multi-stage build where Tailwind runs before source is copied
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Tailwind scans ./src — works because source is copied before build# PROBLEM — .dockerignore excludes files Tailwind needs to scan
# .dockerignore
# src/stories/ <-- If story files contain Tailwind classes, they're missed
# *.test.tsx <-- Test files with Tailwind classes are excludedStorybook — add story files to content paths:
// tailwind.config.js — include Storybook files
module.exports = {
content: [
'./src/**/*.{js,jsx,ts,tsx}',
'./.storybook/**/*.{js,jsx,ts,tsx}', // Storybook config files
'./src/**/*.stories.{js,jsx,ts,tsx}', // Story files
'./src/**/*.mdx', // MDX documentation
],
};Storybook with Tailwind v4 — add PostCSS or Vite plugin to Storybook’s config:
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
// ...
viteFinal: (config) => {
// Add Tailwind v4 Vite plugin to Storybook's Vite config
config.plugins = config.plugins || [];
config.plugins.push(require('@tailwindcss/vite')());
return config;
},
};Fix 6: Dark Mode and Responsive Classes
Variant classes require correct configuration:
// tailwind.config.js — dark mode configuration
module.exports = {
darkMode: 'class', // Dark mode triggered by .dark class on <html>
// or 'media' // Dark mode follows prefers-color-scheme media query
};<!-- Dark mode class variant -->
<div class="bg-white dark:bg-gray-900 text-black dark:text-white">
Works with darkMode: 'class' and <html class="dark">
</div>
<!-- Responsive variants — mobile-first -->
<div class="w-full md:w-1/2 lg:w-1/3">
Full width on mobile, half on md, third on lg
</div>
<!-- Hover, focus, active states -->
<button class="bg-blue-500 hover:bg-blue-600 active:bg-blue-700 focus:ring-2">
Button
</button>Verify dark mode is applied to the HTML element:
// Toggle dark mode
document.documentElement.classList.toggle('dark');
// or
document.documentElement.classList.add('dark');Fix 7: Tailwind v3 JIT Mode Differences from v2
Tailwind v3 made JIT the default and removed some v2 behaviors:
// tailwind.config.js v2 — needed explicit JIT mode
module.exports = {
mode: 'jit', // Had to opt in
purge: ['./src/**/*.{js,jsx}'], // Was 'purge', now 'content'
};
// tailwind.config.js v3 — JIT is default, 'purge' renamed to 'content'
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'], // 'content' not 'purge'
// No 'mode' needed — JIT is always on
};Classes that changed in v3:
<!-- v2 — overflow-ellipsis, overflow-clip -->
<p class="overflow-ellipsis">...</p>
<!-- v3 — renamed to text-ellipsis, text-clip -->
<p class="text-ellipsis">...</p>
<!-- v2 — flex-grow-0, flex-shrink-0 -->
<div class="flex-grow-0 flex-shrink-0">...</div>
<!-- v3 — grow-0, shrink-0 -->
<div class="grow-0 shrink-0">...</div>Arbitrary values (v3 JIT feature):
<!-- JIT allows arbitrary values with [] syntax -->
<div class="w-[372px] bg-[#1a1a2e] text-[14px] mt-[7px]">
Custom sizes without config
</div>
<!-- v2 without JIT: had to add custom values in tailwind.config.js -->
<!-- v3 JIT: use [] for any one-off value -->Fix 8: Debug Missing Classes
Find out why a specific class isn’t generated:
# Check if a class is in the generated CSS
npm run build
grep "bg-blue-500" dist/assets/index-*.css
# If not found — class wasn't detected during content scan
# Run Tailwind CLI manually to test
npx tailwindcss -i ./src/index.css -o ./output.css --watch
# Watch mode shows files being scannedBrowser DevTools — check if the class is defined:
1. Open DevTools → Elements → select the element
2. Check Computed styles — if bg-blue-500 isn't there, it wasn't generated
3. Filter Styles panel for "background" — if empty, class was purged
4. Check if there's a CSS specificity conflict overriding TailwindCheck for CSS specificity conflicts:
/* External CSS overriding Tailwind */
.card {
background-color: white !important; /* Overrides bg-blue-500 */
}
/* Fix — use Tailwind's important modifier */
/* In HTML: class="!bg-blue-500" */
/* Or configure important in tailwind.config.js */// tailwind.config.js — make all utilities important
module.exports = {
important: true, // Adds !important to all utilities
// or
important: '#app', // Scope to specific element ID
};Still Not Working?
Class conflicts from base styles — @tailwind base resets many default browser styles. If elements look wrong even with correct Tailwind classes, the base reset may be interfering. Use @layer base to customize base styles.
Fonts and custom properties — Tailwind’s built-in font families (font-sans, font-mono) reference CSS custom properties. If your CSS resets --font-family, Tailwind fonts may not apply.
Monorepo with shared packages — if a shared UI package in a monorepo uses Tailwind classes, the consuming app must include that package’s source files in its content array. Tailwind does not follow node_modules symlinks by default:
// apps/web/tailwind.config.js
content: [
'./src/**/*.{js,tsx}',
'../../packages/ui/src/**/*.{js,tsx}', // Shared package source
],CDN version has no purging — if you use the Tailwind Play CDN (<script src="https://cdn.tailwindcss.com">), all classes are available but the file size is large and performance is poor. The CDN version is for prototyping only. Switch to a proper build pipeline for production.
PostCSS 7 compatibility layer — some older frameworks ship with PostCSS 7. Tailwind v3+ requires PostCSS 8. If you see Error: PostCSS plugin tailwindcss requires PostCSS 8, upgrade PostCSS or use the @tailwindcss/postcss7-compat package (deprecated but functional).
For related CSS issues, see Fix: CSS Animation Not Working, Fix: CSS Variable Not Working, Fix: Tailwind v4 Not Working, and Fix: CSS Grid Layout Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Tailwind v4 Not Working — @theme, CSS-First Config, PostCSS vs Vite, and v3 Migration
How to fix Tailwind CSS v4 errors — tailwind.config.js ignored, @import 'tailwindcss' not loading, @theme custom values not applied, content scanning misses files, Vite plugin setup, and v3 to v4 migration gotchas.
Fix: Mantine Not Working — Styles Not Loading, Theme Not Applying, or Components Broken After Upgrade
How to fix Mantine UI issues — MantineProvider setup, PostCSS configuration, theme customization, dark mode, form validation with useForm, and Next.js App Router integration.
Fix: View Transitions API Not Working — No Animation Between Pages, Cross-Document Transitions Failing, or Fallback Missing
How to fix View Transitions API issues — same-document transitions, cross-document MPA transitions, view-transition-name CSS, Next.js and Astro integration, custom animations, and browser support.
Fix: Panda CSS Not Working — Styles Not Applying, Tokens Not Resolving, or Build Errors
How to fix Panda CSS issues — PostCSS setup, panda.config.ts token system, recipe and pattern definitions, conditional styles, responsive design, and integration with Next.js and Vite.