C.W.K.
Stream
Lesson 05 of 07 · published

Tailwind Anti-Patterns — What Breaks Quietly

~11 min · anti-patterns, tailwind, scanning

Level 0React Novice
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
The bugs nobody catches in code review. Each of these compiles, runs, and looks fine in one configuration — then fails the moment something changes.

Dynamic class names

className={`bg-${color}-500`} reads naturally but breaks Tailwind's static scanner. The scanner looks for literal class strings in your source; bg-red-500 as a template literal isn't there, so its CSS isn't generated.

Fix: use a map. const colors = { red: 'bg-red-500', blue: 'bg-blue-500' } as const; className={colors[color]}. The literal classes are in the source, the scanner finds them, the runtime indexing produces the right one.

Arbitrary values everywhere

Tailwind supports bg-[#ff8fbe] for one-off hex values. Convenient. But every arbitrary value bypasses your theme. If you have bg-[#ff8fbe] in three files, you've created three duplicate sources of truth. The brand-color rebrand will miss two.

Fix: if a value appears twice, add it to @theme. Arbitrary values are for genuine one-offs.

Bypassing the cascade with !important

Tailwind ships an ! prefix that adds !important: !text-red-500. Use it for genuinely external CSS that can't be reached otherwise (third-party widget overrides). Don't use it to win against your own components — that means your component CSS is in the wrong layer (lesson 3).

Class strings longer than your component logic

A button with 20 utility classes is harder to read than one with five. When the className gets dense, three options:

  1. Extract a custom component class in @layer components (lesson 3).
  2. Group related utilities into named constants: const baseButton = "inline-flex rounded-lg font-medium".
  3. Use cn() with structured arrays and conditions (lesson 4).

Don't accept 20-class lines as 'just how Tailwind looks.' That's giving up on readability.

Reaching for CSS-in-JS

If you're tempted to add styled-components or emotion to a Tailwind project, stop. The two systems duplicate each other and add bundle weight. Either go all-Tailwind (utilities + @layer components for the complex cases) or all-CSS-in-JS — not both.

The static scanner is your friend, not your enemy. Tailwind's build-time scan is what keeps the production CSS small (only used utilities ship). Patterns that defeat the scanner (dynamic strings, runtime class composition) either ship bloated CSS or, more commonly, ship missing CSS that fails silently in production.

Code

Dynamic-class fixes·tsx
// ANTI-PATTERN — scanner misses these
function Badge({ color }: { color: "red" | "green" | "blue" }) {
  return <span className={`bg-${color}-500 text-white`}>!</span>;
}

// FIX 1 — explicit map (typed too)
const badgeColors: Record<"red" | "green" | "blue", string> = {
  red: "bg-red-500",
  green: "bg-green-500",
  blue: "bg-blue-500",
};
function BadgeFixed({ color }: { color: keyof typeof badgeColors }) {
  return <span className={`${badgeColors[color]} text-white`}>!</span>;
}

// FIX 2 — Tailwind safelist (in vite.config or @theme) if you absolutely need
// runtime class names from arbitrary data. Last resort; the map is usually better.
Arbitrary value → theme token promotion·css
/* BEFORE — same hex in three files */
/* <header className="bg-[#ff8fbe]"> ... </header> */
/* <footer className="text-[#ff8fbe]"> ... </footer> */
/* <ring className="ring-[#ff8fbe]"> ... </ring> */

/* AFTER — one source of truth in @theme */
@theme {
  --color-brand: #ff8fbe;
}
/* <header className="bg-brand"> ... </header> */
/* <footer className="text-brand"> ... </footer> */
/* <ring className="ring-brand"> ... </ring> */

External links

Exercise

In your bootstrap project, deliberately introduce two anti-patterns: a bg-${color}-500 dynamic class, and a bg-[#ff8fbe] arbitrary value used in three places. Verify the dynamic class doesn't render styled. Fix both — the dynamic class with a map, the arbitrary value with an @theme token. Confirm both render correctly afterwards.
Hint
After the map fix, check the production build (npm run build && ls dist/assets/) and grep the CSS for bg-red-500. It should be there. With the dynamic class, it wouldn't have been.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.