Why CSS Bugs Are Frustrating
CSS fails silently. There's no error message when a property doesn't work, no stack trace when layout breaks. A missing semicolon or misunderstood behavior can waste hours. This guide covers the most common CSS mistakes, why they happen, and how to fix them permanently.
Layout Mistakes
Mistake 1: Forgetting box-sizing: border-box
The default box-sizing: content-box means width excludes padding and border. An element with width: 100%; padding: 20px will be wider than its parent.
/* BUG */
.element {
width: 100%;
padding: 20px;
border: 1px solid;
/* Actual width = 100% + 40px + 2px = overflows */
}
Parent: [====================] 100%
Child: [========================] 100% + 42px (OVERFLOW)
Fix: Apply border-box globally.
*, *::before, *::after {
box-sizing: border-box;
}
This should be in every project's reset. No exceptions. Every modern CSS framework includes it.
Mistake 2: Margin Collapse Surprises
Vertical margins between adjacent siblings collapse into the larger of the two.
/* BUG: expecting 60px gap, getting 40px */
.box-a { margin-bottom: 40px; }
.box-b { margin-top: 30px; }
/* Actual gap: 40px (not 70px) */
More confusing: a child's top margin can "escape" its parent if the parent has no padding, border, or overflow.
/* BUG: margin appears above the parent, not inside it */
.parent { background: blue; }
.child { margin-top: 40px; }
Expected:
+--blue background--+
| |
| 40px gap |
| [child content] |
+--------------------+
Actual:
40px gap (above parent!)
+--blue background--+
| [child content] |
+--------------------+
Fix: Multiple options.
/* Option 1: padding on parent */
.parent { padding-top: 1px; }
/* Option 2: display: flow-root (cleanest) */
.parent { display: flow-root; }
/* Option 3: overflow */
.parent { overflow: hidden; }
/* Option 4: use flexbox or grid (margins don't collapse) */
.parent { display: flex; flex-direction: column; }
Flexbox and grid containers never have margin collapse. This is one reason modern layouts rarely suffer from this issue.
Mistake 3: Percentage Heights Not Working
/* BUG: height: 50% does nothing */
.parent {
/* no height specified */
}
.child {
height: 50%; /* 50% of what? Parent has no defined height */
}
Percentage heights require the parent to have an explicit height. The chain must go all the way up.
Fix:
/* Option 1: explicit height on parent chain */
html, body { height: 100%; }
.parent { height: 100%; }
.child { height: 50%; }
/* Option 2: use viewport units */
.child { height: 50vh; }
/* Option 3: use flexbox (no percentage needed) */
.parent { display: flex; flex-direction: column; height: 100vh; }
.child { flex: 1; }
Mistake 4: Flex Items Overflowing
By default, flex items refuse to shrink below their content size (min-width: auto). Long text or wide images will overflow the container.
/* BUG: long text overflows */
.flex-container { display: flex; }
.flex-item { flex: 1; }
/* Inside .flex-item: */
.long-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* Truncation doesn't work โ item won't shrink */
}
Fix: Add min-width: 0 to the flex item.
.flex-item {
flex: 1;
min-width: 0; /* allows shrinking below content size */
}
Same issue with grid items โ use minmax(0, 1fr) instead of 1fr:
/* BUG */
.grid { grid-template-columns: 1fr 1fr; }
/* FIX */
.grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
Mistake 5: Using 100vh on Mobile
On mobile browsers, 100vh includes the area behind the URL bar and navigation bar. Content gets hidden behind browser chrome.
/* BUG: hero section taller than visible area on mobile */
.hero {
height: 100vh;
}
Mobile browser:
+--URL bar----------+
| |
| Your content | visible area
| |
+--nav bar-----------+
Hidden content <- behind browser nav
extends here
Fix: Use dynamic viewport units.
.hero {
height: 100vh; /* fallback for older browsers */
height: 100dvh; /* dynamic viewport height โ adjusts with browser chrome */
}
| Unit | Behavior |
|---|---|
| vh | Always includes browser chrome area |
| svh | Smallest possible viewport (browser chrome visible) |
| lvh | Largest possible viewport (browser chrome hidden) |
| dvh | Dynamically adjusts as browser chrome appears/disappears |
Specificity Mistakes
Mistake 6: Specificity Wars
When one rule doesn't work, developers add more selectors or !important to force it:
/* Escalating specificity war */
.nav a { color: blue; }
.nav .link { color: green; } /* override attempt */
.sidebar .nav .link { color: red; } /* more specific */
#sidebar .nav .link { color: purple; } /* ID to win */
#sidebar .nav .link { color: orange !important; } /* nuclear option */
Each override requires even more specificity. The codebase becomes unmaintainable.
Fix: Keep selectors flat and low-specificity.
/* Single class selectors โ all same specificity */
.nav-link { color: blue; }
.nav-link-active { color: green; }
.sidebar-nav-link { color: red; }
Or use BEM:
.nav__link { color: blue; }
.nav__link--active { color: green; }
Mistake 7: Overusing !important
!important overrides all specificity. It should be used only as a last resort.
/* BAD: using !important as default approach */
.button { background: blue !important; }
.button-danger { background: red !important; }
/* Now you need !important everywhere to override anything */
Fix: Restructure selectors instead.
/* GOOD: predictable specificity */
.button { background: blue; }
.button.button-danger { background: red; }
/* Or simply: */
.button-danger { background: red; }
Legitimate !important uses:
- Overriding third-party CSS you can't modify.
- Utility classes that must always win (
.hidden { display: none !important; }). - Print styles.
Mistake 8: Styling with IDs
/* BAD: (1,0,0) specificity โ hard to override */
#header { background: white; }
#nav { color: black; }
/* Need another ID or !important to override */
Fix: Use classes for styling.
/* GOOD: (0,1,0) specificity โ easy to override */
.header { background: white; }
.nav { color: black; }
Reserve IDs for JavaScript (document.getElementById) and anchor links (#section).
Typography and Text Mistakes
Mistake 9: Not Setting Font-Size on Inputs
On iOS Safari, if an input's font-size is less than 16px, the browser automatically zooms in when the user taps the input. This is jarring and disorienting.
/* BUG: causes iOS zoom */
input {
font-size: 14px;
}
/* FIX: minimum 16px */
input, select, textarea {
font-size: 16px; /* or 1rem (if root font-size is 16px) */
}
Mistake 10: Hardcoded Font Sizes
/* BAD: doesn't respect user's browser font size preference */
body { font-size: 14px; }
/* GOOD: respects user preference */
body { font-size: 1rem; } /* 16px by default, but scales with user settings */
Users who set their browser font size to "Large" expect all sites to respect that. Using px for body text overrides their preference.
/* Use rem for font-sizes */
h1 { font-size: 2rem; }
h2 { font-size: 1.5rem; }
p { font-size: 1rem; }
/* Use em for component-relative spacing */
.button { padding: 0.5em 1em; } /* scales with button's font-size */
Mistake 11: Invisible Text on Focus
/* BUG: text appears selected/highlighted but can't be seen */
.dark-card input {
background: #1a1a1a;
color: #1a1a1a; /* same as background โ invisible! */
}
Always test form elements in both light and dark contexts. Ensure text color contrasts with background.
Responsive Mistakes
Mistake 12: Missing Viewport Meta Tag
Without the viewport meta tag, mobile browsers render at ~980px width and zoom out:
<!-- MUST have this in <head> -->
<meta name="viewport" content="width=device-width, initial-scale=1">
Mistake 13: Fixed-Width Containers
/* BUG: overflows on screens narrower than 960px */
.container {
width: 960px;
}
/* FIX: use max-width */
.container {
max-width: 960px;
width: 100%;
margin: 0 auto;
padding: 0 1rem;
}
Mistake 14: Horizontal Scrollbar from 100vw
100vw includes the width of the vertical scrollbar (on Windows/Linux). If the page has a scrollbar, 100vw is wider than the visible area.
/* BUG: horizontal scrollbar */
.full-width {
width: 100vw;
}
/* FIX: use 100% instead */
.full-width {
width: 100%;
}
/* If you need to break out of a container: */
.full-bleed {
width: 100vw;
margin-left: calc(-50vw + 50%);
/* Still may need overflow-x: hidden on body */
}
Mistake 15: Desktop Hover on Touch Devices
:hover states "stick" on touch devices after tapping. The element stays highlighted until the user taps elsewhere.
/* BUG: hover state sticks on mobile */
.card:hover { background: #f0f0f0; }
/* FIX: only apply hover on devices that support it */
@media (hover: hover) and (pointer: fine) {
.card:hover { background: #f0f0f0; }
}
Accessibility Mistakes
Mistake 16: Removing Focus Outlines
/* BAD: removes keyboard focus indicator */
*:focus {
outline: none;
}
/* Users who navigate with keyboard can't see where they are */
Fix: Replace with a custom focus style.
/* Remove default only for mouse clicks, keep for keyboard */
:focus:not(:focus-visible) {
outline: none;
}
/* Custom focus ring for keyboard navigation */
:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
Mistake 17: Low Color Contrast
Text must have sufficient contrast against its background. WCAG 2.1 requirements:
Normal text: 4.5:1 contrast ratio minimum
Large text: 3:1 contrast ratio minimum (18px+ bold, or 24px+)
/* BAD: light gray on white โ fails WCAG */
.text { color: #c0c0c0; background: #ffffff; }
/* Contrast ratio: ~1.6:1 โ FAILS */
/* GOOD: dark gray on white โ passes WCAG */
.text { color: #4b5563; background: #ffffff; }
/* Contrast ratio: ~6.3:1 โ PASSES */
Use browser DevTools or online contrast checkers to verify.
Mistake 18: Small Touch Targets
/* BUG: too small to tap accurately */
.small-link {
font-size: 12px;
padding: 2px;
/* Tappable area: ~16x16px โ too small */
}
/* FIX: minimum 44x44px touch target */
.small-link {
font-size: 12px;
padding: 14px; /* increases touch area */
min-width: 44px;
min-height: 44px;
}
/* Or expand hit area without visual change */
.small-link {
position: relative;
}
.small-link::after {
content: '';
position: absolute;
inset: -10px; /* expands clickable area */
}
Mistake 19: Ignoring prefers-reduced-motion
Some users experience motion sickness from animations:
/* BAD: no motion preference respected */
.element { animation: bounce 1s infinite; }
/* GOOD: respect user's preference */
.element { animation: bounce 1s infinite; }
@media (prefers-reduced-motion: reduce) {
.element { animation: none; }
}
/* BETTER: global reduction */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
Mistake 20: Using Only Color to Convey Information
/* BAD: only color distinguishes error from success */
.error { color: red; }
.success { color: green; }
/* Colorblind users can't distinguish these */
Fix: Add icons, borders, or text labels alongside color.
.error {
color: #dc2626;
border-left: 4px solid #dc2626;
}
.success {
color: #16a34a;
border-left: 4px solid #16a34a;
}
<div class="error">โ Error: Please fix this field</div>
<div class="success">โ Saved successfully</div>
Performance Mistakes
Mistake 21: Animating Expensive Properties
Layout properties (width, height, top, left, margin, padding) trigger the browser to recalculate the layout of potentially hundreds of elements every frame.
/* BAD: animates left (triggers layout every frame) */
.element {
position: relative;
left: 0;
transition: left 0.3s;
}
.element:hover {
left: 100px;
}
/* GOOD: animates transform (GPU-composited, no layout) */
.element {
transition: transform 0.3s;
}
.element:hover {
transform: translateX(100px);
}
Cheap to animate: transform, opacity
Moderate: color, background-color
Expensive: width, height, top, left, margin, padding
Very expensive: box-shadow (large), filter (complex)
Mistake 22: Applying will-change Everywhere
/* BAD: promotes everything to GPU layers, wastes memory */
* {
will-change: transform;
}
/* BAD: permanent will-change on static elements */
.card {
will-change: transform; /* never actually animated */
}
Fix: Apply will-change only when needed, remove after.
/* GOOD: apply only during animation */
.card:hover {
will-change: transform;
transform: translateY(-4px);
}
Mistake 23: Huge Selector Lists
/* BAD: browser evaluates selectors right to left */
body > div > main > section > article > div > p > span a.link { }
/* Browser checks every <a> for .link, then walks up the tree */
/* GOOD: simple selector */
.article-link { }
Deeply nested selectors are slower (though modern browsers have optimized this significantly). More importantly, they're fragile โ any HTML structure change breaks them.
Mistake 24: Unused CSS
Large CSS files slow down initial render. The browser must parse the entire stylesheet before rendering.
Signs of unused CSS:
- CSS file > 50KB for a simple site
- Styles for deleted components
- Imported framework CSS where you use 10% of it
Fix: Audit with DevTools.
Chrome DevTools โ Coverage tab โ Reload โ Shows unused CSS/JS percentage.
Tools to remove unused CSS:
- PurgeCSS (build-time)
- Tailwind CSS (auto-purges)
- Chrome Coverage tab (manual audit)
Mistake 25: Excessive box-shadow or filter
/* EXPENSIVE: multiple complex shadows */
.card {
box-shadow:
0 1px 2px rgba(0,0,0,0.1),
0 4px 8px rgba(0,0,0,0.1),
0 8px 16px rgba(0,0,0,0.1),
0 16px 32px rgba(0,0,0,0.1);
}
/* CHEAPER: single shadow */
.card {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* CHEAPEST: use a pseudo-element with opacity animation */
.card::after {
content: '';
position: absolute;
inset: 0;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
opacity: 0;
transition: opacity 0.3s;
}
.card:hover::after {
opacity: 1;
}
General Mistakes
Mistake 26: Not Using CSS Reset
Browsers have different default styles. Without a reset, your site looks different in Chrome, Firefox, and Safari.
/* Minimal modern reset */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
-moz-text-size-adjust: none;
-webkit-text-size-adjust: none;
text-size-adjust: none;
}
body {
min-height: 100vh;
line-height: 1.5;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
Mistake 27: z-index Guessing
/* BAD: random z-index values */
.dropdown { z-index: 50; }
.modal { z-index: 999; }
.tooltip { z-index: 99999; }
.notification { z-index: 9999999; }
/* Escalation without end */
Fix: Use a z-index scale.
:root {
--z-below: -1;
--z-default: 0;
--z-dropdown: 100;
--z-sticky: 200;
--z-overlay: 300;
--z-modal: 400;
--z-toast: 500;
--z-tooltip: 600;
}
.dropdown { z-index: var(--z-dropdown); }
.modal { z-index: var(--z-modal); }
.tooltip { z-index: var(--z-tooltip); }
Mistake 28: Ignoring the Cascade
CSS stands for Cascading Style Sheets. Understanding source order and specificity prevents most conflicts.
/* Later rules override earlier rules (same specificity) */
.button { color: blue; }
.button { color: red; } /* red wins */
/* File order matters too */
/* base.css loaded before theme.css */
/* theme.css rules win (same specificity) */
Use the cascade intentionally:
- Base/reset styles first (lowest specificity).
- Component styles next.
- Utility/override styles last (win via source order).
Mistake 29: Not Testing in Multiple Browsers
CSS rendering differs across browsers. Common differences:
- Font rendering (antialiasing differs between OS/browser).
- Scrollbar width (Windows shows scrollbars, Mac hides them).
- Default form element styling (every browser differs).
- Newer features (subgrid, container queries, :has()).
Minimum testing:
- Chrome (Chromium)
- Firefox (Gecko)
- Safari (WebKit)
- Mobile Safari (iOS)
- Chrome on Android
CSS Debugging Checklist
When something looks wrong, check these in order:
1. Is the element visible?
- Check display, visibility, opacity, overflow, z-index
- Check if it's behind another element
2. Is the size correct?
- Check box-sizing (border-box vs content-box)
- Check width, height, min/max dimensions
- Check padding and border adding to size
3. Is the position correct?
- Check position value (static, relative, absolute, fixed, sticky)
- Check for positioned ancestor (for absolute elements)
- Check for transform/filter on ancestors (breaks fixed positioning)
4. Is the spacing correct?
- Check margin collapse (vertical margins between siblings/parent-child)
- Check padding vs margin
- Check gap (in flex/grid containers)
5. Is the style being applied?
- Check specificity (DevTools Styles panel โ look for strikethroughs)
- Check source order
- Check for !important overrides
- Check for typos in property names or values
6. Is it a browser-specific issue?
- Test in Chrome, Firefox, Safari
- Check caniuse.com for feature support
- Look for vendor prefixes needed
Quick Reference: Fix Table
| Problem | Common Cause | Fix |
|---|---|---|
| Element wider than expected | content-box sizing | box-sizing: border-box |
| Margin appears outside parent | Margin collapse | display: flow-root on parent |
| Percentage height doesn't work | Parent has no height | Set explicit height on parent chain |
| Flex item overflows | min-width: auto default | min-width: 0 |
| z-index not working | Element is position: static | Add position: relative |
| Sticky not sticking | Parent has overflow | Remove overflow from ancestor |
| Fixed element not fixed | Ancestor has transform | Remove transform or restructure |
| iOS zoom on input focus | Font-size < 16px | font-size: 16px minimum |
| Horizontal scrollbar | 100vw includes scrollbar | Use 100% instead |
| Mobile 100vh too tall | Includes browser chrome | Use 100dvh |
| Hover sticks on mobile | Touch devices lack hover | @media (hover: hover) guard |
Key Takeaways
- Always use
box-sizing: border-boxglobally. It prevents the most common sizing bugs. - Margin collapse is not a bug โ it's by design. Understand it, use
display: flow-rootto prevent it when needed. - Keep specificity flat. Use single-class selectors. Avoid IDs for styling. Avoid
!important. - Use 100dvh instead of 100vh on mobile for full-height sections.
- Never remove focus outlines without providing an alternative. Use
:focus-visible. - Animate only transform and opacity for smooth 60fps animations. Avoid animating width, height, margin, or top/left.
- Test on real devices and multiple browsers. DevTools simulation misses real-world issues.
- Respect user preferences:
prefers-reduced-motion,prefers-color-scheme, browser font size settings. - Use a CSS reset to normalize cross-browser differences.
- When debugging: check specificity first (DevTools Styles panel), then box model, then positioning.