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:
- The file isn’t in
contentconfig. Tailwind only scans files listed incontentoftailwind.config.ts. - Dynamic class names.
className={text-${color}-500}— Tailwind sees the broken string and skips it. - Class is in a comment or string in a non-scanned file.
Fix:
- For new file types/folders: update
contentintailwind.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:
- The element is
position: static(the default).z-indexonly works on positioned elements (relative,absolute,fixed,sticky). - Stacking contexts. A parent with its own
z-index(ortransform,opacity < 1,filter,will-change) creates a new stacking context. Your element’sz-indexonly competes within its parent’s context. - A later sibling has implicitly higher stacking even without
z-index.
Fix:
- Add
position: relative(Tailwind:relative) to givez-indexsomething 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
gapon 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: flexto 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
transformfrom 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:
darkModeconfig setting. Default is"media"(uses OS preference). For class-based toggle: setdarkMode: "class".darkclass not on<html>. Tailwind looks for.darkancestor.- Inline styles or higher-specificity rules overriding. Check computed styles in DevTools.
Fix:
// tailwind.config.ts
darkMode: "class" // or ["class"] in v4Then 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:
- The variable is set on a different scope than where you’re trying to use it.
- The variable is being overridden by a more specific selector.
- 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-mergefor 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:
- Case sensitivity.
bg-[url('/Logo.png')]works on Mac but not Linux (Vercel). - Public folder confusion. Files in
public/are served at root, sopublic/logo.pngis at/logo.png— not/public/logo.png. - 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 properfont-displaysettings 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: hiddenon 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
- Common errors — index 🟩
- Browser errors đźź©
- CSS 🟩 — the textbook
- Tailwind CSS 🟩 🟦
- ui 🟩 🟦
- Dark mode đźź©
- Responsive design đźź©
- Accessibility đźź©
- Color theory for devs đźź©