Fix: Tailwind CSS Classes Not Applying — Styles Missing in Production or Development
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:
contentpaths not configured — Tailwind v3 requires acontentarray intailwind.config.jsspecifying which files to scan for class names. Missing or wrong paths mean Tailwind doesn’t see your class usage.- Dynamic class names — Tailwind scans for complete class name strings.
bg-${color}-500is never seen asbg-blue-500— the scanner finds the template literal but can’t evaluate it. - Incorrect PostCSS setup — Tailwind runs as a PostCSS plugin. Missing
@tailwinddirectives in your CSS or a broken PostCSS config causes no output. - CSS file not imported — your Tailwind CSS file must be imported in your JavaScript entry point.
- Tailwind v2 vs v3 differences — v3 uses JIT (Just-In-Time) by default. Some class names changed.
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: [],
};Verify your config is finding files:
# Use npx tailwindcss CLI to debug what's being scanned
npx tailwindcss --content './src/**/*.{js,jsx}' --output /dev/null --verbose
# Lists all files being scannedCommon 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
// WRONG — quotes inside path are wrong
content: ["./src/**/*.{js,jsx,ts,tsx}"] // Fine — but curly braces need no escaping
// CORRECT — explicit extensions, correct paths
content: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html']Fix 2: Add Required CSS Directives
Your CSS entry file must include the three Tailwind directives:
/* src/index.css or src/globals.css */
@tailwind base; /* Resets and base element styles */
@tailwind components; /* Component classes (e.g., from plugins) */
@tailwind utilities; /* Utility classes (bg-blue-500, p-4, etc.) */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';For Tailwind v4 (new CSS-first config):
/* Tailwind v4 uses @import instead of @tailwind directives */
@import "tailwindcss";
/* Config in CSS, not JS */
@theme {
--color-primary: oklch(70% 0.2 240);
}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: Verify PostCSS Configuration
Tailwind requires PostCSS. Check the setup:
// postcss.config.js — must include tailwindcss plugin
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
// Or with explicit paths (if config file not in root)
module.exports = {
plugins: {
tailwindcss: { config: './tailwind.config.js' },
autoprefixer: {},
},
};Verify packages are installed:
npm list tailwindcss postcss autoprefixer
# If missing:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p # Creates tailwind.config.js and postcss.config.jsCheck for PostCSS version conflicts:
# PostCSS 8 is required for Tailwind v3+
npm list postcss
# Should show [email protected]
# If an older version is installed:
npm install -D postcss@latestFix 5: 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 — decoration-slice, decoration-clone (didn't exist) -->
<!-- v3 — box-decoration-slice, box-decoration-clone -->
<!-- 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 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: 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 scanned
# Test with a minimal HTML file
echo '<div class="bg-blue-500 p-4">test</div>' > test.html
# Add test.html to content:
# content: ['./test.html']
# Then check if bg-blue-500 appears in outputBrowser 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?
Vite + Tailwind — Vite uses its own asset pipeline. Ensure tailwindcss is in postcss.config.js (not just in vite.config.js). Vite processes CSS through PostCSS automatically if the config file exists.
Create React App (CRA) — CRA uses its own PostCSS setup. Add Tailwind via @craco/craco or react-app-rewired to customize PostCSS, or use the CRACO Tailwind plugin.
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.
For related CSS issues, see Fix: CSS Animation Not Working and Fix: CSS Variable 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: 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.
Fix: UnoCSS Not Working — Classes Not Generating, Presets Missing, or Attributify Mode Broken
How to fix UnoCSS issues — Vite plugin setup, preset configuration, attributify mode, icons preset, shortcuts, custom rules, and integration with Next.js, Nuxt, and Astro.