Late one Thursday in June 2026 a team pushed a pricing page to production and every call-to-action button rendered plain gray. Locally, in staging, in the PR preview, the buttons were brand-blue. In production they were dead. The culprit was one line: className={\`bg-${brand}-600\`}. A cached local build had the class from an earlier hardcoded version; the clean production build never generated it. One template literal, a broken launch, a very long evening.
That is the thing about Tailwind mistakes. They rarely throw an error. They just quietly ship something wrong. Below are the ones we still fix most often in real codebases, each with the exact code that resolves it, all current for Tailwind CSS v4.3.2, the latest stable release as of July 2026.
What are the most common Tailwind CSS mistakes?
The most common Tailwind CSS mistakes fall into three buckets: fighting the scanner (dynamic class names, dead detection config), abandoning the utility model (over-applying @apply, arbitrary-value soup, re-inventing utilities the framework already ships), and carrying v3 habits into v4 (JS-first config, manual content arrays that no longer do anything). The list is ordered by how often each one bites teams in review.
1. Building class names with string interpolation
This is the number-one Tailwind CSS mistake, the one that cost the team above their evening. Tailwind scans your source as plain text, so a class it never sees as a complete string never gets generated. bg-${color}-500 produces nothing.
// Broken: Tailwind never sees "bg-red-500" as a literal
<div className={`bg-${color}-500`} />
// Fixed: map to full, static class strings
const COLORS = {
red: "bg-red-500",
green: "bg-green-500",
blue: "bg-blue-500",
};
<div className={COLORS[color]} />Keep every class a complete literal somewhere in your source. When you truly need a runtime value, reach for an inline style or a CSS variable, not a constructed utility name.
2. Overusing @apply
@apply feels like a fix for repetition, but wrapping every component in @apply flex items-center gap-2 rounded-lg ... just moves your styles back into a stylesheet and throws away the reason you adopted Tailwind. My honest take: in a 2026 review, a component-layer file full of @apply is a code smell, not a pattern.
/* Avoid: a hand-rolled component layer for everything */
.btn {
@apply inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-white;
}Prefer a real component that owns its classes. In React or Vue, extract a <Button> and let props drive variants. Reserve @apply for the rare cases where you cannot control the markup, like styling third-party HTML or prose from a CMS.
3. Reaching for arbitrary values by default
Arbitrary values like w-[738px] or text-[#3a3a3a] are an escape hatch, not a workflow. When half your markup uses bracket syntax, you have quietly opted out of your own design system, and every spacing and color decision becomes a one-off nobody can audit.
<!-- Off the scale: three near-identical, unrelated values -->
<div class="w-[738px] p-[19px] text-[#3a3a3a]">
<!-- On the scale: tokens your team can reason about -->
<div class="w-[46rem] p-5 text-gray-700">Extend your theme when you need a value repeatedly, and save arbitrary values for genuine one-offs like a hero image's magic-number height. If you type the same bracket value twice, it belongs in @theme.
4. Hand-rolling utilities v4 already ships
This one is new for 2026 and it is everywhere. Tailwind added first-party utilities faster than most teams noticed, so people still write custom CSS for things that now have a class. In v4.1 that meant text-shadow-* and mask-* utilities; in v4.3, released June 12 2026, first-party scrollbar styling landed.
<!-- Old habit: custom CSS in a stylesheet for a scrollbar -->
<div class="custom-scroll">…</div>
<!-- v4.3: native utilities, right in the markup -->
<div class="scrollbar-thin scrollbar-thumb-gray-400 scrollbar-track-transparent">…</div>Before you write a plugin or a one-off stylesheet, check the docs for the utility. Text shadows, masks, scrollbars, and @container-size height-aware container queries are all built in now.
5. Still configuring everything in JavaScript
Tailwind v4 moved configuration into CSS with the @theme directive, and tailwind.config.js is no longer the default entry point. Teams that upgraded but kept a sprawling JS config miss the biggest v4 win: design tokens that are real CSS variables, usable outside Tailwind.
/* app.css — the v4 way */
@import "tailwindcss";
@theme {
--color-brand: oklch(0.62 0.19 255);
--font-display: "Inter", sans-serif;
--spacing-gutter: 1.5rem;
}Every token here becomes a --color-brand variable you can read in plain CSS, inline styles, or JS. Keep a JS config only for plugins that still require it.
6. Maintaining a content array that does nothing
In v3 you listed every template path in content: [...] so the scanner knew where to look. Tailwind v4 detects your source automatically and ignores anything in .gitignore. Teams copy an old config forward, keep editing a dead content array, and then wonder why their safelist behaves oddly.
If you need a file outside the default detection (say a compiled package in node_modules), use the explicit source directive:
@import "tailwindcss";
@source "../node_modules/@acme/ui/dist";Delete the dead array, and add @source only for paths the automatic scan genuinely misses.
7. Killing focus outlines with outline-none
Slapping outline-none on buttons and inputs to hide the "ugly" focus ring is an accessibility regression, not a style choice. Keyboard users lose all sense of where they are, and you have shipped a WCAG failure. This is the most common finding in every frontend audit we run.
<!-- Broken: no visible focus for keyboard users -->
<button class="outline-none">Save</button>
<!-- Fixed: replace the ring, don't remove it -->
<button class="outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2">
Save
</button>If you remove the default outline, supply a visible focus-visible state in the same breath. Our web accessibility checklist covers the full set of interactive-state requirements.
8. Reinventing conditional classes by hand
Concatenating class strings with template literals and ternaries is error-prone: double spaces, conflicting utilities like px-2 px-4 winning unpredictably, unreadable logic. Two small libraries solve it. As of July 2026, tailwind-merge supports Tailwind v4.0 through v4.3 (use v2.6.0 if you are still on v3).
Approach | Handles conditionals | Resolves conflicts | Verdict |
|---|---|---|---|
Template literals | Poorly | No | Avoid |
| Yes | No | Good for simple toggles |
| Via clsx | Yes | Pair them |
| Yes | Yes | Best for components |
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
const cn = (...inputs) => twMerge(clsx(inputs));
// cn("px-2", isWide && "px-4") -> "px-4", no conflictWrap the two into a single cn() helper and use it everywhere you build classes conditionally. The shadcn/ui ecosystem standardized on exactly this helper.
Which Tailwind version am I actually on?
Half these mistakes are really "v3 habits on a v4 codebase," so know your line. Here is the current 2026 picture.
Version | Released | Headline feature |
|---|---|---|
v4.0 | Jan 2026 | CSS-first |
v4.1 | Apr 2026 | |
v4.2 | Feb 2026 | Webpack plugin, new palettes, 3.8x faster rebuilds |
v4.3 | Jun 2026 | First-party scrollbar utilities, |
The official upgrade guide runs a codemod that catches most of the config-level traps automatically.
A quick pre-commit checklist
Run through this before you push a Tailwind change. It catches most of the mistakes above in under a minute:
- No class name is built with string interpolation.
@applyappears only where you cannot control the markup.- Arbitrary
[...]values are rare and truly one-off. - You checked for a native utility before writing custom CSS.
- Shared tokens live in
@theme, not scattered in brackets. - No dead
contentarray lingers in a v4 config. - Every
outline-nonehas a matchingfocus-visiblestate. prettier-plugin-tailwindcsssorts class lists automatically.- Conditional classes go through a
cn()helper.
For the wider frontend picture, pair this with our CSS container queries guide, the Core Web Vitals checklist, and our React state management comparison, or browse the full web development category.
Frequently Asked Questions
Why do my dynamic Tailwind classes not work in production?
Because Tailwind scans source as plain text and only generates classes it sees as complete strings. An interpolated name like text-${size} is never a full literal, so nothing is generated. Map your values to full static class strings, or safelist the exact classes you build at runtime.
Is @apply bad practice in Tailwind?
Not inherently, but overusing it is. @apply for every component pushes styles back into stylesheets and discards the colocation and consistency that make Tailwind worthwhile. Use real components with props for variants, and reserve @apply for markup you cannot edit, like CMS prose or third-party widgets.
Do I still need a tailwind.config.js in Tailwind v4?
Usually no. v4 configures through the CSS @theme directive and detects your content automatically, so a JS config is optional. Keep one only for plugins that still require it, and move your colors, fonts, and spacing tokens into @theme so they become real CSS variables.
What is new in Tailwind CSS v4.3?
Released June 12 2026, v4.3 adds first-party scrollbar utilities (scrollbar-thin, scrollbar-thumb-*, scrollbar-track-*, scrollbar-gutter-*), a @container-size utility for height-aware container queries, and new color palettes. The latest patch, v4.3.2, shipped June 29 2026.



