Tailwind Class Not Applying — Why It Happens and How to Fix It

Quick answer

💡Tailwind classes that appear in your HTML but have no effect in the browser are almost always caused by a content path mismatch in tailwind.config.js. The JIT engine only generates CSS for classes it finds by scanning the files listed in the content array. If your file is outside those paths, or if you construct class names dynamically, the class is never generated and the browser receives no matching CSS rule.

Error symptoms

  • A Tailwind class applied in JSX has no visible effect in the browser
  • The class appears in the HTML source but is absent from the generated CSS bundle
  • Styles work in development but disappear in the production build
  • A dynamically constructed class like text-${color}-500 never generates CSS
  • Dark mode classes like dark:bg-slate-800 have no effect even when dark mode is active
  • @apply fails with a rule not found error in a CSS file processed by PostCSS

Common causes

  • The content array in tailwind.config.js does not include the file where the class is used
  • Dynamic class construction: 'text-' + color prevents JIT from detecting the full class name
  • The Tailwind directives are missing from the main CSS file or in the wrong order
  • Dark mode is set to class mode but the dark class is not added to the html element
  • File is at a path outside the glob pattern in the content array
  • Using Tailwind v4 syntax in a v3 config or vice versa — the config format changed significantly

When it happens

  • After adding a new page or component file outside the src/ directory covered by the content glob
  • When a developer builds class names dynamically using template literals or string concatenation
  • After upgrading from Tailwind v3 to v4 without reading the migration guide
  • When dark mode toggle logic adds the dark class to the body but Tailwind config expects it on html

Examples and fixes

Tailwind's JIT scanner reads source files as text — it cannot evaluate JavaScript at scan time.

Dynamic class name breaks JIT scanning

❌ Wrong

// React component — dynamic class is never detected
const statusColors = {
  active: 'green',
  inactive: 'gray',
  error: 'red',
};

function StatusBadge({ status }) {
  const color = statusColors[status];
  return (
    <span className={`bg-${color}-100 text-${color}-700 px-2 py-1 rounded`}>
      {status}
    </span>
  );
}

✅ Fixed

// Explicit full class names — JIT detects all three
const statusClasses = {
  active: 'bg-green-100 text-green-700',
  inactive: 'bg-gray-100 text-gray-700',
  error: 'bg-red-100 text-red-700',
};

function StatusBadge({ status }) {
  return (
    <span className={`${statusClasses[status]} px-2 py-1 rounded`}>
      {status}
    </span>
  );
}

The JIT engine scans source files using regex-like text matching to find class names. It does not execute your JavaScript. When you write 'bg-' + color or use a template literal with an interpolated segment, the full class name bg-green-100 never appears as a literal string in any file, so the scanner cannot detect it and never generates the CSS rule. The fix is always to ensure the complete class name appears as a string somewhere in your source. Use a lookup object keyed by the full class names, which are all literal strings that the scanner can find.

Files outside the content glob are invisible to JIT — their classes generate no CSS.

Content path missing a file location

❌ Wrong

// tailwind.config.js — only covers src/
module.exports = {
  content: [
    './src/**/*.{js,jsx,ts,tsx}',
  ],
  theme: { extend: {} },
  plugins: [],
};

// pages/dashboard.tsx — outside src/, classes stripped
import React from 'react';
export default function Dashboard() {
  return <div className="p-8 bg-slate-50 min-h-screen">...</div>;
}

✅ Fixed

// tailwind.config.js — covers both src/ and pages/
module.exports = {
  content: [
    './src/**/*.{js,jsx,ts,tsx}',
    './pages/**/*.{js,jsx,ts,tsx}',
    './components/**/*.{js,jsx,ts,tsx}',
  ],
  theme: { extend: {} },
  plugins: [],
};

// pages/dashboard.tsx — now scanned, classes generated
import React from 'react';
export default function Dashboard() {
  return <div className="p-8 bg-slate-50 min-h-screen">...</div>;
}

Tailwind v3 and v4 both require the content configuration to cover every file that uses Tailwind classes. The glob pattern './src/**/*.{js,jsx,ts,tsx}' matches any file under src/ recursively, but it does not match files at pages/ or components/ at the project root. Any class that only appears in those uncovered files is not included in the generated CSS bundle. The fix is to add each top-level directory containing Tailwind classes to the content array. When in doubt, add more paths — unused paths do not increase bundle size, they just add a small amount of scan time during build.

How JIT Scanning Determines Which Classes to Generate

Tailwind's Just-in-Time engine generates CSS on demand rather than shipping the entire utility class library. In Tailwind v2, the approach was to ship all utilities and use PurgeCSS to strip unused ones. In v3, the JIT engine replaced this model entirely: it scans your source files to discover which class names you actually use, then generates only those CSS rules. This produces dramatically smaller bundles — typically 5–20 KB of gzipped CSS instead of hundreds of kilobytes.

The scanning process is text-based. Tailwind reads each file listed in the content configuration as a string and uses pattern matching to find anything that looks like a Tailwind class name. It does not parse or execute your JavaScript, HTML, or template files. This is why the content paths matter so much — a file not listed in content is never read, and its classes are never generated.

The scanner looks for patterns that match complete Tailwind class names: strings that match utility prefixes followed by values from your theme config. It will find p-4 in className="p-4" or class="p-4" or even in a comment or a data attribute. It will not find p-4 if the string is constructed at runtime as 'p-' + size, because the text p-4 never appears in the source file.

In Tailwind v4, the configuration format changed significantly. Instead of a tailwind.config.js file, v4 uses CSS-first configuration with @theme in your main CSS file. The content detection is also smarter in v4 — it uses a Rust-based scanner that is faster and can detect patterns in more contexts. However, v3 and v4 configs are not compatible, and upgrading without reading the migration guide is a common cause of all classes disappearing.

Diagnosing Which Classes Are Missing and Why

Start in the browser's DevTools. Open the Elements panel, click on the element that should have the class applied, and check the Styles pane. If the class name appears in the class attribute but there is no corresponding rule in the Styles list, the class was not generated. The browser received HTML with the class name but no CSS that matches it.

Search the generated CSS bundle for the missing class name. In a Next.js or Vite project, look in the .next/static/css/ or dist/assets/ directories for the compiled CSS file. Open it and search for the class. If it is absent, the JIT scanner did not detect it. If it is present, the issue is specificity — some other rule is overriding it — and you need to check the cascade, not the Tailwind config.

To verify your content paths, add a console.log to your build process or use Tailwind CLI with the --watch flag and observe which files it scans. Alternatively, open tailwind.config.js and mentally walk through each glob pattern, then check whether the file containing the missing class is matched by at least one of those patterns. Use a glob tester tool if you are unsure how the pattern resolves.

For dynamic class names, search your source code for partial class name strings. If you find 'text-' + variable or template literals like text-${variable}-500, those are the culprits. The solution is always to ensure the full class name appears as a complete string somewhere — either in the component file, a constants file, or in the safelist array in tailwind.config.js.

Step-by-Step Fixes for Each Root Cause

For missing content paths: open tailwind.config.js and add the directory of the affected file to the content array. Use a glob pattern that covers the whole directory: './pages/**/*.{js,jsx,ts,tsx}'. Restart the dev server after changing the config — Tailwind does not always detect config changes in watch mode.

For dynamic class names: refactor the component to use a lookup object where keys map to complete class name strings. Every key in the object should be a complete Tailwind class, not a partial. Alternatively, add the specific classes to the safelist array in tailwind.config.js: safelist: ['bg-green-100', 'text-green-700', 'bg-red-100', 'text-red-700']. The safelist forces those classes to be generated regardless of whether they appear in source files.

For @apply not working: ensure the CSS file containing @apply is processed by PostCSS with the Tailwind plugin. The @apply rule only works in PostCSS-processed files — it does not work in inline styles or in CSS-in-JS solutions. Also check that the utility you are trying to @apply is in the utilities layer; base and component layer utilities may require different syntax.

For dark mode not working: check your tailwind.config.js for darkMode: 'class'. This setting requires a dark class on the html element (not body) to activate dark mode styles. If your theme toggle adds the class to body, move it to document.documentElement. If you want dark mode to follow the OS preference automatically, change to darkMode: 'media' — no class toggling required.

When diagnosing which classes would be generated without committing to a full build, run npx tailwindcss --content './src/**/*.tsx' --dry-run in your terminal. This command scans the specified content glob and prints a preview of the CSS that would be generated, letting you verify that the classes you expect are detected before running a complete production build.

Tricky Situations: Safelisting and Third-Party Classes

Safelisting supports patterns as well as explicit strings. The safelist array in tailwind.config.js can include objects with a pattern property: { pattern: /bg-(red|green|blue)-(100|200|500)/ }. This generates all matching combinations without listing each one individually. Use patterns when you have a finite set of dynamic values that you can express as a regex.

Third-party components that pass className props can also cause missing classes. If a library component accepts a className prop and you pass Tailwind classes to it, those classes should be detected normally by the scanner as long as the class names appear as literal strings in your source. The issue arises when the library generates class names internally using concatenation — in that case, use the safelist or a custom CSS override.

Storybook and testing environments often run Tailwind separately from the main application. If your stories or test files are not included in the content array, Tailwind classes used in those files will not be generated for the Storybook build. Add .storybook/**/*.{js,jsx,ts,tsx} and stories/**/*.stories.{js,jsx,ts,tsx} to your content paths, or configure Storybook to use the same Tailwind CSS entry point as your app.

In Tailwind v4, the CSS entry file uses @import 'tailwindcss'; rather than the three @tailwind directives. If you are on v4 and still using @tailwind base; @tailwind components; @tailwind utilities;, those directives are silently ignored and your styles will not load at all. The migration guide at tailwindcss.com covers the v3-to-v4 changes in detail — it is worth reading fully before upgrading any production project confidently.

Common Mistakes in Tailwind Project Setup

Forgetting to add @tailwind base; @tailwind components; @tailwind utilities; to the main CSS file is the most common setup mistake. These three directives inject Tailwind's reset, component layer, and utility layer respectively. Without them, even if Tailwind generates CSS, the file containing those utilities is never imported. In a Next.js project, this goes in globals.css; in a Vite project, it typically goes in main.css or index.css.

Using NEXT_PUBLIC_ prefix on secrets or API URLs is unrelated to Tailwind, but a related mistake is wrapping Tailwind class names in template literals unnecessarily. Writing className={`flex items-center`} when you mean className="flex items-center" is fine — both work. But writing className={`flex ${isActive ? 'bg-blue-500' : 'bg-gray-500'}`} is also fine, because both bg-blue-500 and bg-gray-500 appear as complete strings. The mistake is breaking the class name across an interpolation boundary.

Overriding Tailwind utilities with custom CSS using the same selector often fails because Tailwind utilities are imported last in the CSS cascade and have higher source order than earlier custom rules. If you need to override a Tailwind utility, use a more specific selector, apply the @layer directive to control where your custom styles land, or simply add an additional class to the element with higher specificity.

Plugins in tailwind.config.js that are listed but not installed as npm packages cause the entire Tailwind build to fail silently or with a cryptic module not found error. Always verify that require('@tailwindcss/forms') and require('@tailwindcss/typography') are listed in your package.json dependencies and installed with npm install before adding them to the plugins array.

Preventing Tailwind Class Issues in Production

Configure your content paths conservatively to cover every location that could contain Tailwind classes. Include not just your main component directories but also any scripts, utilities, or config files that might pass class names as strings. Over-inclusive content paths add a small amount of build time but prevent the frustrating experience of classes working locally and disappearing in production.

Adopt the convention of always writing complete Tailwind class names in source code. Document this in your team's contribution guide: never construct class names using string concatenation or template literal interpolation. Instead, use a lookup object or an array of conditional class names where each entry is a complete, static string. Libraries like clsx and classnames make this pattern ergonomic while keeping all class names as detectable string literals.

For design systems or component libraries distributed as npm packages, enable the component package scanning option or publish a prebuilt CSS file rather than relying on the consuming application to scan your package. Tailwind v4 introduced better support for pre-built package CSS, but in v3, consuming apps need to add your package's files to their content array: './node_modules/@yourcompany/ui/**/*.js'.

Run your production build locally before deploying when you make significant changes to the component tree or add new Tailwind classes. Production builds apply all optimizations including content scanning, which can reveal class coverage gaps that are masked in development mode. A simple npm run build followed by a visual inspection of the production build in the browser catches most class-stripping issues before they reach users.

Quick fix checklist

  • Confirm the file containing the class is covered by a glob in the content array
  • Inspect the generated CSS bundle and search for the missing class name
  • Check that class names are complete literal strings, not dynamically constructed fragments
  • Verify @tailwind base; @tailwind components; @tailwind utilities; are in the main CSS entry file
  • For dark mode, confirm the dark class is on html, not body
  • Restart the dev server after any change to tailwind.config.js
  • For dynamic classes, add them to the safelist with explicit strings or a regex pattern
  • Check whether you are on Tailwind v3 or v4 and that your config matches that version

Related guides

Frequently asked questions

Why do Tailwind classes work in development but disappear in production?

Development mode in most Tailwind setups uses a broader scanning mode or generates all classes to speed up iteration. Production builds apply strict JIT scanning using only the content paths in your config. If a class only appears in a file not covered by the content glob, it is generated in dev but stripped in production. Check that every file directory used in your project is included in the content array in tailwind.config.js.

How do I use dynamic class names safely with Tailwind?

Use a lookup object where the values are complete Tailwind class name strings, never partial fragments. The scanner finds literal strings, so every full class name must appear somewhere as a string in your source. If the set of dynamic values is bounded, list them explicitly. If the set is large or data-driven, add the specific classes to the safelist array in your Tailwind config with an explicit list or a regex pattern.

What is the Tailwind safelist and when should I use it?

The safelist forces specific classes to be generated even if the scanner does not find them in source files. Use it when class names are constructed dynamically from database values, API responses, or user input — cases where the complete class string cannot appear in source code. Add the safelist entry as an array of strings or pattern objects in tailwind.config.js. Avoid using safelist as a crutch for statically generated classes that should be written out in full.

Does Tailwind v4 work differently from v3?

Yes, significantly. Tailwind v4 uses CSS-first configuration with @theme blocks in your CSS file instead of tailwind.config.js. The import syntax changed from the three @tailwind directives to @import 'tailwindcss'. The content detection is Rust-based and faster. Existing v3 configs are not forward-compatible, so upgrading requires following the official migration guide. Do not mix v3 and v4 syntax in the same project.

Why is @apply not working in my CSS file?

@apply only works in CSS files that are processed by PostCSS with the Tailwind plugin active. If your CSS file is not in the PostCSS pipeline — for example, it is loaded directly via a script tag or processed by a different loader — @apply will not be recognized. Ensure your build configuration routes .css files through PostCSS and that the Tailwind plugin appears in the PostCSS config. Also ensure you are @apply-ing utility classes, not base or component layer styles.

How do I set up Tailwind dark mode correctly?

For class-based dark mode, set darkMode: 'class' in tailwind.config.js and add the dark class to the html element (document.documentElement.classList.add('dark')) when the user enables dark mode. Adding dark to body instead of html is a common mistake that prevents dark: prefixed classes from activating. For automatic OS-preference-based dark mode with no JavaScript required, use darkMode: 'media' — Tailwind then uses prefers-color-scheme instead of a class selector.

Can I use Tailwind with CSS Modules?

Yes. CSS Modules and Tailwind are complementary. In a .module.css or .module.scss file, you can use @apply to compose Tailwind utilities into a scoped class name. The module gives you scoping and the Tailwind utilities give you the design system values. Ensure the CSS Module file is processed by PostCSS with Tailwind. You can also use Tailwind utility classes directly alongside CSS Module classes in the same className attribute.

Why is my Tailwind config change not taking effect?

Tailwind reads tailwind.config.js at startup in watch mode, but some changes — especially to the content array or plugins — require restarting the dev server to take effect. Changes to the theme extension do not always trigger a full rebuild in watch mode. When in doubt, stop your dev server and start it fresh after any config change. Also ensure you are editing the correct config file if your project has multiple configuration files.

How does Tailwind interact with third-party component libraries?

Third-party component libraries that accept className props and render your Tailwind classes as-is work fine — the scanner finds the class names in your source files where you pass the prop. Libraries that generate class names internally using concatenation are problematic. For those, use the safelist to force generation of the specific classes the library uses, or add the library's source directory to the content array if it ships readable source files.

All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-06.