CSS / Tailwind surprises

Status: đźź© COMPLETE Last updated: 2026-06-21 Plain-English tagline: Not always errors, but the styling surprises that waste hours. z-index wars, flex sizing, Tailwind purge gotchas, the silent ways CSS breaks.


What this is

A reference for CSS and Tailwind behaviors that aren’t errors but produce wrong results — silent visual bugs, things that “should work” but don’t. For genuine browser console errors, see Browser errors 🟩.


”My Tailwind class doesn’t apply in production but works in dev”

What it means: Tailwind’s content scan didn’t find the class at build time, so it wasn’t generated.

Common causes:

  1. The file isn’t in content config. Tailwind only scans files listed in content of tailwind.config.ts.
  2. Dynamic class names. className={text-${color}-500} — Tailwind sees the broken string and skips it.
  3. Class is in a comment or string in a non-scanned file.

Fix:

  • For new file types/folders: update content in tailwind.config.ts:
    content: ["./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./lib/**/*.{ts,tsx}"]
  • For dynamic class names: use a lookup map so the full class names appear in source:
    const colorClasses = { red: "text-red-500", blue: "text-blue-500" };
    <div className={colorClasses[color]}>
  • For “safelist” of classes that must always exist: add to Tailwind config:
    safelist: ["bg-red-500", "bg-blue-500"]

“z-index doesn’t work”

What it means: You set z-index: 9999 but the element still appears behind something else.

Common causes:

  1. The element is position: static (the default). z-index only works on positioned elements (relative, absolute, fixed, sticky).
  2. Stacking contexts. A parent with its own z-index (or transform, opacity < 1, filter, will-change) creates a new stacking context. Your element’s z-index only competes within its parent’s context.
  3. A later sibling has implicitly higher stacking even without z-index.

Fix:

  • Add position: relative (Tailwind: relative) to give z-index something to work with.
  • For stacking-context issues, raise the parent’s z-index too. Or restructure so the element isn’t trapped in a low-priority context.

”Flex children won’t shrink below their content”

What it means: A child of display: flex with long content (like text) refuses to shrink, causing overflow or pushing siblings.

Cause: Flex children default to min-width: auto, which means “at least as wide as my content.”

Fix:

.flex-child {
  min-width: 0;       /* allow shrinking */
  overflow: hidden;   /* or specific behavior */
}

Tailwind: min-w-0. Add it to flex children that contain text you want to truncate or wrap.

This is one of the most common “Tailwind is broken” complaints — Tailwind is fine; CSS flexbox just has this default.


”Margin between elements collapsed”

What it means: You added margin-bottom: 16px to one element and margin-top: 16px to the next, and got 16px total instead of 32px.

Cause: Vertical margins of adjacent block elements collapse — the larger one wins; they don’t add.

Fix:

  • Use gap on the parent if it’s a flex/grid container:
    <div class="flex flex-col gap-4">  <!-- 16px always -->
  • Use padding instead of margin for one side.
  • Add display: flex to the parent to disable margin collapse.

”My styled element is missing styles after deploy”

What it means: Same as “Tailwind class doesn’t apply in production” but framed differently. Almost always: class purge issue.

Fix: see “Tailwind class doesn’t apply in production” above.


”Position: fixed inside a transformed parent doesn’t fix to viewport”

What it means: Your modal with position: fixed; top: 0; left: 0 is positioning relative to a parent, not the viewport.

Cause: A parent with transform (or perspective, filter, will-change, contain: paint, certain backdrop-filter) creates a new containing block for fixed-position descendants.

Fix:

  • Render the modal in a portal — outside the transformed parent:
    import { createPortal } from "react-dom";
    return createPortal(<Modal />, document.body);
  • Or remove the transform from the parent. Often an animation transform applied to the wrong level.

shadcn/ui’s Dialog uses portals automatically.


”Tailwind dark mode classes don’t apply”

What it means: You wrote dark:bg-slate-900 but the dark color doesn’t show.

Common causes:

  1. darkMode config setting. Default is "media" (uses OS preference). For class-based toggle: set darkMode: "class".
  2. dark class not on <html>. Tailwind looks for .dark ancestor.
  3. Inline styles or higher-specificity rules overriding. Check computed styles in DevTools.

Fix:

// tailwind.config.ts
darkMode: "class"   // or ["class"] in v4

Then ensure <html class="dark"> is set (next-themes handles this automatically).


”My CSS variable doesn’t update”

What it means: You changed a --var value but the styles using it don’t react.

Common causes:

  1. The variable is set on a different scope than where you’re trying to use it.
  2. The variable is being overridden by a more specific selector.
  3. You’re using var(--name) outside the scope that defines it.

Fix:

:root {
  --primary: #2563eb;       /* global */
}
.dark {
  --primary: #93c5fd;       /* dark override */
}
.card {
  background: var(--primary);   /* picks whichever wins */
}

Verify in DevTools → Computed → which --primary is winning.


”Tailwind classes get reordered weirdly”

What it means: Two utility classes with overlapping properties produce surprising results because of source-order, not what you intended.

Example: <div className="p-4 p-8"> — which wins? Whichever comes later in the generated CSS, which Tailwind sorts internally. The DOM order of class names doesn’t determine specificity.

Fix:

  • Use tailwind-merge for runtime deduplication. Especially helpful when composing classes from props:
    import { twMerge } from "tailwind-merge";
    twMerge("p-4", "p-8")  // → "p-8"
  • Use shadcn’s cn() helper (which uses tailwind-merge) — Bible Quest’s standard pattern.

”100vh is too tall on mobile”

What it means: Mobile browsers’ address bars cause 100vh to include the area behind the bar, making content overflow.

Fix: use the new dynamic viewport units:

.full-height { height: 100dvh; }  /* dynamic — accounts for browser chrome */

Tailwind v4: h-dvh. Tailwind v3: requires a custom utility.

Fallback for older browsers: progressive enhancement with 100vh as fallback.


”Padding on inline elements doesn’t push surrounding content”

What it means: You added padding to an <span> or <a> and the surrounding text overlaps.

Cause: Inline elements lay out within text flow; padding doesn’t reserve vertical space.

Fix: make it inline-block (Tailwind: inline-block) or flex. Or just use a block element if appropriate.


”Background images / fonts don’t load in production”

What it means: Image / font path that works locally returns 404 on Vercel.

Common causes:

  1. Case sensitivity. bg-[url('/Logo.png')] works on Mac but not Linux (Vercel).
  2. Public folder confusion. Files in public/ are served at root, so public/logo.png is at /logo.png — not /public/logo.png.
  3. Build hash mismatch. Older deployments cached by browser.

Fix: match case exactly. Reference assets via /path (root-relative) for public/ files.


”Custom font shows as fallback font, then flashes to custom”

What it means: “Flash of unstyled text” (FOUT). Browser uses fallback while custom font loads.

Fix:

  • Use next/font — Next.js handles font loading with proper font-display settings and self-hosting.
  • For external fonts: preload them with <link rel="preload" as="font" />.
  • For acceptable fallback flash: use font-display: swap (the modern default).

”My component looks fine until I scroll”

What it means: Layout breaks when content overflows. Often: sticky headers losing their backgrounds, scrollbar appearing changes width, transformed children leaving artifacts.

Common causes:

  • Missing background on sticky header
  • Scrollbar width pushing content
  • overflow: hidden on a parent eating focus rings

Fix: test with content longer than the viewport early. Set explicit backgrounds on sticky elements. Use scrollbar-gutter: stable to reserve scrollbar space.


See also


Sources