Fix: Tailwind CSS Classes Not Applying
Quick Answer
How to fix Tailwind CSS classes not applying to your HTML elements. Covers content config paths, purge and safelist, class conflicts and specificity, dynamic class names, PostCSS config, @apply issues, dark mode config, and JIT mode problems.
The Error
You add a Tailwind class to an element:
<div class="bg-blue-500 text-white p-4 rounded-lg">
Hello world
</div>The element renders with no styling. No background color, no padding, no rounded corners. You inspect it in DevTools and the classes simply don’t exist in the generated CSS. No errors in the console, no build failures — the styles are just silently missing.
This happens in every Tailwind setup: Tailwind CLI, PostCSS, Vite plugin, Next.js, Nuxt, Astro, and others. The root cause varies, but the result is the same — your utility classes produce no CSS output.
Why This Happens
Tailwind CSS works differently from traditional CSS frameworks. Instead of shipping every possible utility class in a massive stylesheet, Tailwind scans your source files for class names and generates CSS only for the ones you actually use. This is what makes Tailwind fast and small in production.
The flip side: if Tailwind can’t find your class names during that scan, it won’t generate CSS for them. The scan is purely text-based — Tailwind doesn’t execute your code, parse your templates, or understand your framework’s rendering logic. It runs a regex over your files looking for strings that match utility class patterns.
This means classes can go missing for several reasons:
- Tailwind isn’t scanning the right files
- The class name is constructed dynamically at runtime
- Another stylesheet overrides Tailwind’s output
- The build pipeline isn’t processing Tailwind at all
- Configuration errors prevent specific utilities from being generated
The following fixes cover every common cause, from the most frequent to the more obscure edge cases.
Fix 1: Check Your Content Configuration Paths
This is the number one cause. Tailwind doesn’t know which files to scan for class names, so it generates an empty (or near-empty) stylesheet.
Tailwind CSS v3 uses a content array in tailwind.config.js:
// tailwind.config.js
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx,html,vue,svelte,astro}",
"./index.html",
],
theme: {
extend: {},
},
plugins: [],
};Tailwind CSS v4 (released 2025) dropped tailwind.config.js for most use cases and uses automatic content detection based on your CSS @import or @source directives. If you upgraded to v4 and your old config is being ignored, that’s the problem.
For v4, your CSS entry file should look like:
@import "tailwindcss";If Tailwind v4’s automatic detection misses some files, use @source to explicitly include paths:
@import "tailwindcss";
@source "../components/**/*.tsx";
@source "../layouts/**/*.astro";Common mistakes with content paths:
- Missing file extensions. If you use
.astro,.svelte, or.vuefiles but only list{js,ts,jsx,tsx}, those files won’t be scanned. - Wrong base directory. Paths are relative to the config file location. If your
tailwind.config.jsis in the project root but your templates are insrc/, you need"./src/**/*.{js,jsx,tsx}", not"./**/*.{js,jsx,tsx}"(which might work but is wasteful) or"./components/**/*.jsx"(which misses other directories). - Monorepo paths. If you have a shared component library in
../../packages/ui/src/**/*.tsx, you must include it explicitly. Tailwind won’t follow imports across packages. - Node modules. Third-party component libraries that use Tailwind classes (like Flowbite, daisyUI components, or Headless UI examples) need their paths in your content config too:
"./node_modules/flowbite/**/*.js".
Verify your setup by adding a class you know should work — like bg-red-500 — to a file that’s definitely in your content paths. If that class shows up in DevTools, the issue is with which files are being scanned, not with Tailwind itself.
Pro Tip: Run your build and inspect the generated CSS file directly. In most setups you can find it in your output directory or check the
<style>tags in DevTools. If the file is extremely small (under 1 KB), Tailwind is scanning zero files. If it’s a few KB, it’s scanning some files but missing others.
Fix 2: Stop Using Dynamic Class Names
Tailwind’s scanner is text-based, not runtime-aware. It searches for complete class name strings in your source code. If you construct class names with string concatenation or template literals, Tailwind will never find them.
This does not work:
// Tailwind CANNOT detect these classes
const color = "blue";
<div className={`bg-${color}-500`} />
// Also broken
const size = props.large ? "8" : "4";
<div className={`p-${size}`} />
// Also broken
const classes = ["bg", "red", "500"].join("-");Tailwind sees the literal strings bg-${color}-500 and p-${size} in your source code. Those don’t match any utility class pattern, so no CSS is generated.
Instead, use complete class name strings and select between them:
// This works — Tailwind finds both full class names in the source
const bgColor = props.primary ? "bg-blue-500" : "bg-gray-500";
<div className={bgColor} />
// This also works — all possible classes are visible as complete strings
const paddingMap = {
sm: "p-2",
md: "p-4",
lg: "p-8",
};
<div className={paddingMap[props.size]} />The key rule: every class name must appear as a complete, unbroken string somewhere in a file that Tailwind scans. It doesn’t have to be in the same file or even in executable code — Tailwind just needs to see the string.
If you have classes coming from an API or database at runtime and you genuinely cannot list them in your source code, use the safelist (covered in Fix 3).
This same principle applies if you’re using a utility like clsx or classnames. Those libraries work fine with Tailwind — they select between class strings at runtime — but the class strings themselves must still be complete and visible in source code. If you find your classes not applying when using conditional logic, also check for issues with CSS specificity that might cause unexpected overrides.
Fix 3: Use Safelist for Classes That Must Always Exist
Sometimes you need classes that can’t appear as static strings in your source files. Content from a CMS, user-selected themes, or dynamically generated status badges are common examples. For these, use the safelist.
Tailwind v3 — add a safelist array in your config:
// tailwind.config.js
module.exports = {
content: ["./src/**/*.{js,jsx,tsx}"],
safelist: [
// Exact class names
"bg-red-500",
"bg-green-500",
"bg-yellow-500",
// Pattern matching with regex
{
pattern: /bg-(red|green|blue|yellow)-(100|500|900)/,
variants: ["hover", "dark"],
},
// Safelist all text colors
{
pattern: /text-(red|green|blue)-(400|500|600)/,
},
],
theme: {
extend: {},
},
};Tailwind v4 — use the @source directive with inline() to safelist classes directly in CSS:
@import "tailwindcss";
@source inline("bg-red-500 bg-green-500 bg-yellow-500 text-white");Warning: Don’t safelist everything. The whole point of Tailwind’s scanning is to keep your CSS bundle small. Safelisting hundreds of classes defeats that purpose and bloats your stylesheet.
Fix 4: Fix Class Conflicts and Specificity Issues
Your Tailwind classes might be generating CSS just fine, but another stylesheet is overriding them. This is a classic CSS specificity problem.
Check the order of your CSS imports. Tailwind’s utilities need to come after your base/reset styles but can be overridden by styles that load later:
/* Correct order */
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "./custom-components.css"; /* Your component styles */
@import "tailwindcss/utilities"; /* Utilities last = highest priority */If you import a third-party CSS file after Tailwind’s utilities layer, that file can override your utility classes.
Check for !important in other stylesheets. Any rule with !important beats Tailwind’s utilities unless you enable Tailwind’s own important option:
// tailwind.config.js (v3)
module.exports = {
important: true, // Adds !important to all utilities
// ...
};A less aggressive approach — scope Tailwind to a specific parent selector:
// tailwind.config.js (v3)
module.exports = {
important: "#app", // .bg-blue-500 becomes #app .bg-blue-500
// ...
};Check for inline styles. Inline style attributes always beat class-based rules (unless the class uses !important). If a JavaScript library or framework sets inline styles on your elements, Tailwind classes for those same properties will lose.
Use DevTools to diagnose. Inspect the element, look at the “Styles” panel, and check if your Tailwind class is present but crossed out. If it is, another rule is winning the specificity battle. The panel shows you exactly which rule is overriding it.
Common Mistake: Using both Tailwind classes and custom CSS on the same element for the same property. For example, having
class="p-4"while a CSS file sets.card { padding: 20px; }. Whichever rule has higher specificity wins, and the result depends on your stylesheet order. Pick one approach per property and stick with it.
Fix 5: Fix Your PostCSS Configuration
If you’re using Tailwind as a PostCSS plugin (the most common setup for frameworks like Next.js, Nuxt, and Vite), a broken PostCSS config means Tailwind never processes your CSS at all.
Verify postcss.config.js (or postcss.config.cjs / postcss.config.mjs) exists in your project root:
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};Common PostCSS issues:
- Missing the file entirely. Some frameworks auto-detect PostCSS config. Others (like a plain Vite project) require it explicitly.
- Wrong format. If your
package.jsonhas"type": "module", you needpostcss.config.mjswithexport defaultsyntax, orpostcss.config.cjswithmodule.exports. A mismatch causes a silent failure. This is similar to module resolution issues in Vite where the config format matters. - Plugin order matters.
tailwindcssmust come beforeautoprefixer. If you havepostcss-import, it should come beforetailwindcss. - Conflicting PostCSS configs. If both
postcss.config.jsand apostcssfield inpackage.jsonexist, one may be ignored depending on your toolchain.
For Tailwind v4 with Vite, PostCSS is often unnecessary. The @tailwindcss/vite plugin handles everything:
// vite.config.js
import tailwindcss from "@tailwindcss/vite";
export default {
plugins: [tailwindcss()],
};If you recently migrated to v4 and still have a postcss.config.js referencing the old tailwindcss PostCSS plugin, remove it and switch to the framework-specific plugin.
Fix 6: Fix @apply Not Working
The @apply directive lets you compose Tailwind utilities inside custom CSS classes. When it breaks, you get a build error or the styles silently don’t apply.
Build error: “Cannot apply unknown utility class”:
Error: Cannot apply unknown utility class "bg-brand-500"This means you’re trying to @apply a custom class you defined in theme.extend but the directive can’t find it. In Tailwind v3, make sure the utility exists in your config:
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
brand: {
500: "#6366f1",
},
},
},
},
};Then in your CSS:
.btn-primary {
@apply bg-brand-500 text-white px-4 py-2 rounded;
}In Tailwind v4, custom theme values are defined differently using @theme:
@import "tailwindcss";
@theme {
--color-brand-500: #6366f1;
}
.btn-primary {
@apply bg-brand-500 text-white px-4 py-2 rounded;
}The @apply directive doesn’t work across file boundaries. If you define a custom utility in one CSS file and try to @apply it in another, it will fail. Each file is processed independently. Move your @apply rules into the same file as your Tailwind imports, or use CSS custom properties instead.
@apply with component libraries. If you’re using @apply with classes from a plugin like @tailwindcss/typography or daisyUI, make sure the plugin is properly installed and listed in your config’s plugins array. Missing plugins mean missing utilities, which means @apply can’t find them.
Fix 7: Fix Dark Mode Configuration
Your dark mode classes (dark:bg-gray-900, dark:text-white) render in light mode or don’t work at all. The issue is almost always the dark mode strategy configuration.
Tailwind v3 supports two strategies:
// tailwind.config.js
// Strategy 1: Class-based (manual toggle)
module.exports = {
darkMode: "class",
// ...
};
// Strategy 2: Media query (follows OS preference)
module.exports = {
darkMode: "media", // This is the default
// ...
};With "class" mode, the dark class must be on the <html> element (not <body>):
<html class="dark">
<body>
<div class="dark:bg-gray-900">This will have a dark background</div>
</body>
</html>If you put class="dark" on <body> or a <div>, dark mode won’t activate for "class" mode. The selector Tailwind generates is .dark .dark\:bg-gray-900, and it looks for the .dark class on an ancestor element — specifically, the html element by default.
With "media" mode, the dark: variants use @media (prefers-color-scheme: dark). No class is needed — it follows the user’s OS setting. If you’re testing in a browser, use DevTools to emulate dark mode: open the Command Palette (Ctrl+Shift+P / Cmd+Shift+P), search for “Render > Emulate CSS prefers-color-scheme: dark”.
Tailwind v4 uses the @media (prefers-color-scheme: dark) strategy by default. To switch to class-based:
@import "tailwindcss";
@variant dark (&:where(.dark, .dark *));Warning: If you’re using a framework like Next.js with a theme provider (like next-themes), make sure the provider’s attribute matches your Tailwind config. next-themes uses class on <html> by default, which matches Tailwind’s "class" strategy. If they’re mismatched, dark mode will seem broken. The resulting UI inconsistency is conceptually similar to hydration mismatches in Next.js — the server and client render different things.
Fix 8: Fix JIT Mode and Build Cache Problems
Tailwind v3 uses JIT (Just-in-Time) mode by default. In earlier versions (v2.1+), it was opt-in. JIT compiles CSS on demand, which is fast but can cache aggressively. Tailwind v4 also uses a JIT-style compiler internally.
Stale cache. If you added new classes and they’re not showing up, clear the cache:
# Delete the node_modules/.cache directory
rm -rf node_modules/.cache
# For Next.js, also clear its cache
rm -rf .next
# For Nuxt
rm -rf .nuxt
# For general build tools
rm -rf dist .cacheThen restart your dev server. Many build tools (Vite, Webpack, Next.js) cache PostCSS output aggressively. A stale cache means Tailwind’s output from a previous run is served even after you change your config or content paths.
Dev server not watching files. If your dev server doesn’t reload when you add new Tailwind classes, the file watcher might not cover the files you’re editing. This is especially common in monorepo setups where components live outside the project root. Make sure your content paths include those external directories.
Build works, dev doesn’t (or vice versa). This usually means different configs are used for dev and production. Check if you have environment-specific config overrides. Also check if your framework uses different build pipelines — for example, Next.js uses its own Webpack/Turbopack setup that might handle PostCSS differently than running npx tailwindcss directly. If you’re hitting module resolution problems alongside Tailwind issues, the root cause may be your bundler config rather than Tailwind itself.
Arbitrary values not generating. JIT mode supports arbitrary values like bg-[#1da1f2] or p-[17px]. If these don’t work:
- Check for spaces inside the brackets:
bg-[#1da1f2]works,bg-[ #1da1f2 ]does not. - Check for missing CSS units:
w-[100]is invalid,w-[100px]is correct. - Some characters need escaping or alternative syntax. Underscores replace spaces:
bg-[url('/img/hero.png')]works,content-['hello_world']renders as “hello world”.
Tailwind v2 (legacy). If you’re still on Tailwind v2 without JIT, you’re using the PurgeCSS-based workflow. This means all utilities exist in dev mode (large CSS file) but are purged in production. If classes are missing only in production, your purge config (the v2 equivalent of content) is wrong:
// tailwind.config.js (v2)
module.exports = {
purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
// ...
};If you’re on v2, strongly consider upgrading. Tailwind v3 and v4 have better performance, smaller output, and simpler configuration.
Still Not Working?
If none of the above fixes solved your issue, try these less obvious causes:
Tailwind isn’t installed or is the wrong version. Verify it:
npx tailwindcss --help
# or check your version
npx tailwindcss --versionIf you get “command not found” or an unexpected version, reinstall:
npm install -D tailwindcss@latest postcss autoprefixerYour CSS entry file doesn’t import Tailwind. Somewhere in your CSS, you need the Tailwind directives. For v3:
@tailwind base;
@tailwind components;
@tailwind utilities;For v4:
@import "tailwindcss";If these directives are missing, Tailwind has nothing to inject its styles into. Without a proper entry point, the build pipeline won’t process Tailwind at all.
Your CSS file isn’t imported by your app. Even if the CSS file exists and has the right directives, your JavaScript entry point must import it:
// main.js or App.jsx or _app.tsx
import "./globals.css"; // or whatever your CSS file is namedCheck your framework’s docs for where CSS imports should go. In Next.js, global CSS can only be imported in _app.tsx or layout.tsx. In Vite, import it in main.js.
Browser extension interference. Some extensions inject CSS that conflicts with Tailwind. Test in an incognito window with all extensions disabled.
Typos in class names. Tailwind classes are exact. bg-blue-500 works; bg-Blue-500, bgblue500, or bg-blue500 do not. Use your editor’s Tailwind IntelliSense extension to autocomplete class names and catch typos early. Install the Tailwind CSS IntelliSense extension for VS Code — it provides autocomplete, linting, and hover previews for every utility class.
Using a CSS-in-JS library alongside Tailwind. Libraries like styled-components or Emotion generate styles at runtime and inject them into the document head. If they load after Tailwind’s stylesheet, their styles can override Tailwind utilities. If they load before, Tailwind wins. Check the order of <style> tags in your document’s <head>.
Tailwind Preflight resets your styles. Tailwind’s @tailwind base (v3) or the base layer in v4 includes Preflight — an opinionated CSS reset. It removes default margins, sets border-style: solid, makes images display: block, and more. If you expect default browser styles (like list bullets or heading sizes), Preflight has removed them. Either add them back with Tailwind utilities (list-disc, text-2xl) or disable Preflight:
// tailwind.config.js (v3)
module.exports = {
corePlugins: {
preflight: false,
},
};If you’ve gone through every fix and classes still don’t apply, create a minimal reproduction: a fresh project with only Tailwind installed. Add your classes there. If they work, the issue is in your project’s specific configuration — compare the two setups to find the difference.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Angular ExpressionChangedAfterItHasBeenCheckedError
How to fix ExpressionChangedAfterItHasBeenCheckedError in Angular caused by change detection timing issues, lifecycle hooks, async pipes, and parent-child data flow.
Fix: AWS Lambda Unable to import module / Runtime.ImportModuleError
How to fix the AWS Lambda Runtime.ImportModuleError and Unable to import module error caused by wrong handler paths, missing dependencies, layer issues, and packaging problems.
Fix: CMake Could Not Find Package Configuration File
How to fix the CMake error 'Could not find a package configuration file' for find_package, covering CMAKE_PREFIX_PATH, dev packages, vcpkg, Conan, module mode, toolchain files, and cross-compilation.
Fix: C# async deadlock — Task.Result and .Wait() hanging forever
How to fix the C# async/await deadlock caused by Task.Result and .Wait() blocking the synchronization context in ASP.NET, WPF, WinForms, and library code.