Skip to content

CSS Fundamentals Footguns

Layout bugs, specificity wars, and visual regressions that waste hours when you do not understand the underlying model.


1. Not Setting box-sizing: border-box Globally

You set a sidebar to width: 300px and add padding: 20px. The sidebar renders at 340px wide (content + padding on both sides). Your two-column layout breaks because the sidebar and main content now exceed the container width. You add overflow: hidden to the parent, which hides part of the content instead of fixing the root cause.

Fix: Add the universal reset at the top of your base stylesheet:

*, *::before, *::after {
    box-sizing: border-box;
}

With border-box, the 300px width includes padding and border. This is how every modern CSS framework works. Without it, every width calculation requires manual math that inevitably breaks.

Under the hood: The default content-box model dates back to the original CSS1 spec (1996), which defined width as content width only. Internet Explorer 5.5 actually used border-box by default (the "IE box model bug"), and the web community later realized IE's interpretation was more intuitive. The box-sizing property was introduced in CSS3 specifically to let developers opt into the behavior IE had "wrong." Today, every CSS reset and framework starts with border-box.


2. Fighting Specificity Wars with !important

A third-party library sets .button { color: blue; }. You add .button { color: red; } but it does not work because the library's selector is .theme .button (higher specificity). You add !important to force it. Later, another component needs a different button color but cannot override your !important. Someone adds !important there too. Within weeks, half the stylesheet is !important and nothing can be overridden cleanly.

Fix: Match or exceed the conflicting selector's specificity instead of using !important. If the library uses .theme .button, override with .theme .button or a more specific selector. Use browser DevTools to see which selector is winning (the Styles panel shows overridden rules with a strikethrough). Reserve !important only for utility classes that must always win (e.g., .hidden { display: none !important; }).


3. Forgetting That z-index Requires Positioning

You add z-index: 100 to a dropdown menu but it still renders behind other content. You increase it to 9999, then 999999 — nothing changes. The element has position: static (the default), and z-index has no effect on statically positioned elements.

Fix: Set position: relative (or absolute/fixed/sticky) on the element before applying z-index. Then check for stacking context boundaries — an ancestor with opacity, transform, filter, or will-change creates an isolated stacking context that limits z-index to that subtree.


4. Margin Collapse Causing "Missing" Space

You put a <h1> with margin-top: 40px inside a <div> wrapper. The 40px margin appears above the wrapper, not inside it. The wrapper appears to have no top spacing. You add margin-top to the wrapper too, making 80px of intended spacing but only 40px renders (the larger margin wins).

Fix: Margin collapse occurs when there is nothing between adjacent margins — no padding, border, or content. Break the collapse by adding any of: - padding-top: 1px on the parent - border-top: 1px solid transparent on the parent - overflow: hidden or overflow: auto on the parent - display: flow-root on the parent (cleanest modern fix) - Use gap in flex/grid layouts instead of margins (gap never collapses)


5. Using px for Everything on Responsive Layouts

Your status page looks perfect on a 1920px monitor. On a 13-inch laptop, text is tiny and buttons are too small to tap on mobile. You hardcoded every dimension in pixels. A user with impaired vision increases their browser font size — nothing changes because px ignores the user's font size preference.

Fix: Use rem for font sizes and spacing (respects root font size and user preferences). Use % or viewport units (vw, vh) for layout widths. Use px only for borders, shadows, and other decorative elements that should not scale. Set a sensible root: html { font-size: 16px; } then use rem everywhere: 1rem = 16px, 0.875rem = 14px, 1.5rem = 24px.


6. CSS File Served with Wrong MIME Type

You deploy a new status page. The HTML loads but is completely unstyled. The browser console shows: Refused to apply style from 'style.css' because its MIME type ('text/plain') is not a supported stylesheet MIME type. Your web server (nginx, Apache, or a static file server) is serving .css files as text/plain.

Fix: Verify your web server has correct MIME type mappings. In nginx:

# /etc/nginx/mime.types must include:
types {
    text/css  css;
}
# Or in the location block:
location ~* \.css$ {
    add_header Content-Type text/css;
}

Test with: curl -sI http://localhost/static/style.css | grep content-type


7. Absolute Positioning Without a Positioned Ancestor

You position a tooltip with position: absolute; top: 100%; left: 0 expecting it to appear below its trigger button. Instead, it appears at the bottom-left of the entire page. The tooltip positions relative to the nearest positioned ancestor — since no ancestor has position set, it falls through to the document body.

Fix: Add position: relative to the tooltip's parent container. This creates a positioning context without moving the parent:

.tooltip-wrapper { position: relative; }
.tooltip { position: absolute; top: 100%; left: 0; z-index: 10; }

Always verify the positioning ancestor in DevTools — it is rarely what you assume when debugging someone else's CSS.


8. Flex Children Overflowing Instead of Wrapping

You create a row of cards with flexbox. On a narrow screen, the cards overflow horizontally instead of wrapping to the next line. You add overflow-x: auto as a band-aid, giving a horizontal scroll. Users on mobile see one and a half cards and do not realize there are more.

Fix: Add flex-wrap: wrap to the flex container. Also set a min-width or flex-basis on the children so they know when to wrap:

.card-row {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
}
.card {
    flex: 1 1 300px; /* grow, shrink, min 300px before wrapping */
    min-width: 0;    /* prevents content from forcing overflow */
}

The min-width: 0 on flex children is critical — the default min-width: auto prevents flex items from shrinking below their content size, which is the hidden cause of most flex overflow bugs.


9. Text Overflowing Fixed-Width Containers

A monitoring dashboard displays hostnames in a table column. A hostname like very-long-hostname-for-the-staging-environment-us-east-1.internal.example.com overflows its cell, pushing the entire table layout off-screen. Or a URL in a log viewer breaks the page width.

Fix: Apply text overflow handling:

.hostname-cell {
    max-width: 250px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* For text that should wrap without breaking layout: */
.log-line {
    overflow-wrap: break-word;
    word-break: break-all; /* for URLs and long strings */
}

In flex containers, also add min-width: 0 to the flex child — without it, the child cannot shrink below its content width.


10. Forgetting Vendor Differences in Default Styles

Your internal tool looks fine in Chrome but has different spacing, font sizes, and form element styling in Firefox and Safari. Default browser stylesheets differ between vendors. A <button>, <input>, or <select> can look completely different across browsers without explicit styling.

Fix: Use a CSS reset or normalize.css at the top of your stylesheet. For internal tools, a minimal reset is sufficient:

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
       Roboto, sans-serif; line-height: 1.5; }
button, input, select, textarea { font: inherit; }

The font: inherit on form elements is critical — without it, buttons and inputs use the browser's default font instead of your chosen font.


11. Loading CSS After Page Content Causes Flash of Unstyled Content

Your MkDocs documentation site shows raw unstyled HTML for a half second before the CSS loads. This "FOUC" (Flash of Unstyled Content) happens because the stylesheet is loaded at the bottom of the page or via JavaScript, so the browser renders HTML before CSS is available.

Fix: Always put <link rel="stylesheet"> tags in the <head>, before any content. Never load CSS asynchronously unless you have a specific reason and a fallback. For critical above-the-fold styles, inline them in a <style> tag in the head. For large CSS files, use <link rel="preload" href="style.css" as="style"> to trigger early download.


12. Using Color Alone for Status Indication

Your status page uses green, yellow, and red circles to indicate service health. A colorblind user (8% of men) cannot distinguish red from green. During an outage, they cannot tell which services are down.

Fix: Always pair color with another visual indicator — text labels, icons, or patterns:

.status-ok::before { content: "OK"; }
.status-warn::before { content: "DEGRADED"; }
.status-down::before { content: "DOWN"; }
/* Or use distinct shapes/icons alongside color */

Test with Chrome DevTools: Rendering tab → "Emulate vision deficiencies" → select deuteranopia or protanopia to see what colorblind users see.