CSS Best Practices & Common Mistakes
The conventions that separate maintainable CSS from a stylesheet nobody wants to touch — and the mistakes every beginner makes at least once.
Consistent Naming Is the Cheapest Insurance Policy You Can Buy
The CSS Architecture & Naming Conventions module covered BEM (block__element--modifier) in depth — the block, element, and modifier convention that keeps class names predictable and self-documenting. This module does not re-teach BEM; it assumes you have it, and looks at what actually goes wrong when a team doesn't stick to a convention consistently, because that is where real stylesheets rot.
.card { border-radius: 8px; }
.CardHeader { font-weight: bold; }
.card-body-text { color: #333; }
.cardFooterBtn { padding: 8px 16px; }
.is-active-card { border-color: blue; }Nothing here is individually wrong — each rule works in isolation. The problem is that five different casing styles (kebab-case, PascalCase, camelCase) and five different relationship conventions are mixed in a single file. A new engineer joining the project cannot predict what the next class name will look like, cannot search-and-replace confidently, and cannot tell from the name alone whether .is-active-card is a state modifier on .card or an unrelated, independent class.
.card { border-radius: 8px; }
.card__header { font-weight: bold; }
.card__body { color: #333; }
.card__footer-btn { padding: 8px 16px; }
.card--active { border-color: blue; }Every class name now tells you two things at a glance: which block it belongs to (card), and its relationship to that block (__header is a sub-part, --active is a state variant). This is not about BEM specifically being the one correct answer — it is about picking one convention and applying it everywhere, so the naming itself carries information instead of being noise a reader has to work around.
One selector, one responsibility
A closely related habit: avoid classes that describe more than one concern at once, like .blue-bold-14px-header. That class is unreusable the moment the design changes the color, and it forces you to either rename it everywhere it is used or leave a misleading name in place. Name classes for what the element is or does (.card__header), not for how it currently happens to look — the "how it looks" belongs entirely inside the rule's declarations, where it can change freely without touching the markup.
Building a Spacing Scale With Custom Properties
A "magic number" in CSS is any hardcoded value that appears with no explanation for why that specific number was chosen — margin-bottom: 13px, padding: 22px 17px, gap: 9px. Every one of these numbers was probably reasonable in the exact spot it was written, tweaked in DevTools until it "looked right." The problem shows up later: with no system behind them, these numbers multiply across a codebase until nothing lines up with anything else, and nobody can tell which values are load-bearing versus accidental.
.card { padding: 18px; margin-bottom: 22px; }
.card__header { padding-bottom: 11px; }
.sidebar { padding: 16px 20px; }
.button { padding: 9px 15px; }
.modal { padding: 24px; gap: 14px; }The CSS Custom Properties module covered var() and --custom-property as the mechanism — this module applies that mechanism to the single most common source of layout inconsistency: spacing. The fix is a spacing scale — a small, fixed set of spacing values, each expressed as a custom property, that every component draws from instead of inventing its own numbers.
:root {
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 24px;
--space-6: 32px;
--space-7: 48px;
--space-8: 64px;
}.card { padding: var(--space-4); margin-bottom: var(--space-5); }
.card__header { padding-bottom: var(--space-3); }
.sidebar { padding: var(--space-4) var(--space-5); }
.button { padding: var(--space-2) var(--space-4); }
.modal { padding: var(--space-5); gap: var(--space-3); }Nothing in the rendered page looks meaningfully different at first — the win is structural, not visual. A designer who says "let's make spacing slightly tighter across the whole product" is now a one-line change to the scale's values, not a search across dozens of files for every hardcoded number close to the one being adjusted. And because the scale is a small, closed set, a reviewer can immediately spot a rule that breaks from it — padding: 13px stands out as an obvious outlier next to padding: var(--space-3) in a way it never would sitting among other raw pixel values.
Scales are not just for spacing
The same idea extends to font sizes, border-radius values, and shadow depths — any property where a project benefits from a small, deliberate set of choices instead of unlimited freedom. A type scale (--text-sm, --text-base, --text-lg, --text-xl...) solves the exact same "13px here, 15px there, nobody remembers why" problem that a spacing scale solves for margins and padding.
Specificity Wars and the !important Escalation Trap
The CSS Selectors Deep Dive module covered how specificity is calculated — IDs beat classes, classes beat elements, and !important overrides the calculation entirely. This module is about what happens on a real team over real time when specificity is not managed deliberately: a slow, self-inflicted arms race that ends with !important scattered through the codebase and nobody confident they can safely remove it.
/* Week 1 — a normal class rule */
.button { background: blue; }
/* Week 3 — a new engineer needs the button red inside the sidebar,
and reaches for a more specific selector instead of a new class */
.sidebar .button { background: red; }
/* Week 6 — someone needs it blue again inside a specific card, and the
previous fix's specificity now has to be beaten */
.sidebar .card .button { background: blue; }
/* Week 9 — out of patience, someone reaches for !important
to guarantee a win, regardless of what else is in the file */
.button { background: green !important; }Each individual change was locally reasonable — every engineer was just trying to make one button the right color without breaking anything else they could see. But nobody was managing specificity as a system, so each fix silently raised the bar for every future fix. Once one rule uses !important, overriding it later requires either another !important (with equal specificity, the later one in source order wins) or an inline style — and the arms race compounds from there.
/* One class per intended appearance, applied directly at the markup level */
.button { background: blue; }
.button--danger { background: red; }
.button--secondary { background: green; }
/* <button class="button button--danger">Delete</button>
The correct variant is chosen by adding a class, never by
out-specificity-ing an existing rule. */Flat, low-specificity selectors as the default habit
The sustainable version of the fix above generalizes into a habit: prefer a single class selector (specificity 0-1-0) for almost everything, and treat any selector that chains multiple classes or nests several levels deep as a smell worth questioning. BEM naming directly supports this — because .card__header is already unambiguous on its own, there is rarely a real need to write .card .card__header or anything deeper just to "be safe."
element /* 0-0-1 e.g. p, div */
.class /* 0-1-0 e.g. .card__header — stay here for almost everything */
.class.class /* 0-2-0 e.g. .card.card--active */
#id /* 1-0-0 e.g. #main-nav — avoid for styling entirely */
style="..." /* 1-0-0-0 inline — avoid for styling entirely */
!important /* effectively overrides the entire calculation above */Splitting Stylesheets Without Losing Track of the Cascade
A single 4,000-line styles.css file is unmanageable, but splitting CSS across files introduces its own risk: the cascade does not care about file boundaries, and a rule in buttons.css can still be silently overridden by a rule in legacy.css loaded afterward. A sustainable structure keeps files small and keeps load order predictable.
styles/
base.css /* resets, custom property scales, global element defaults */
layout.css /* page-level grid/flex containers, header, footer */
components/
button.css
card.css
modal.css
nav.css
utilities.css /* small, single-purpose helper classes, loaded last */The ordering matters as much as the splitting: base.css first (so every later file can rely on the custom properties it defines), component files next in no particular order relative to each other (since well-scoped BEM classes should not collide), and utilities.css loaded last, since utility classes are meant to intentionally win over component-level styles when applied.
.card__header could plausibly be in five different files, the organization is working against you, not for you.Box Model, Selectors & Positioning Mistakes Seen Across This Entire Track
The following mistakes each map back to a specific earlier module in this track — they are collected here because, individually, each one is easy to explain, but together they account for a disproportionate share of real CSS bugs reported in code review.
Forgetting box-sizing: border-box
.box {
width: 300px;
padding: 20px;
border: 2px solid #333;
}
/* Rendered width is 300 + 20*2 + 2*2 = 344px, not 300px — padding and
border are ADDED on top of the declared width under the default
box-sizing: content-box, exactly as covered in the Box Model module. */The near-universal fix, applied once globally rather than per-element, is covered fully in the Box Model module: * { box-sizing: border-box; } — width and height then include padding and border, matching what most engineers actually expect intuitively.
Overqualifying selectors
div.container ul.nav-list li.nav-item a.nav-link { color: blue; }
/* Every element type prefix is redundant once the class is already
unique enough to identify the element — and this selector is now
locked to that exact tag structure, breaking the moment <ul> becomes
<nav> or <li> becomes <div> during a later refactor. */.nav-link { color: blue; }Using position: absolute without a positioned ancestor
.card { padding: 16px; } /* no position set */
.card__badge { position: absolute; top: 8px; right: 8px; }
/* Without position: relative on .card, .card__badge positions itself
against the nearest ANCESTOR that has one — which may be the
<body> element, placing the badge somewhere far from the card
entirely, exactly the Display & Positioning module's warning. */.card { padding: 16px; position: relative; }
.card__badge { position: absolute; top: 8px; right: 8px; }Layout, Responsive & Unit Mistakes Seen Across This Entire Track
Reaching for margin where gap belongs
.row { display: flex; }
.row > * { margin-right: 16px; }
.row > *:last-child { margin-right: 0; } /* an extra rule just to undo the last one */.row { display: flex; gap: 16px; }Covered across both the Flexbox and Grid modules: gap spaces items between them only, with no extra rule needed to zero out the last item's trailing margin — a fix that used to require the exact :last-child workaround shown above before gap had broad Flexbox support.
Using px for font-size instead of rem
body { font-size: 16px; }
h1 { font-size: 32px; }
/* A user who increases their browser's default font size for readability
sees NO change here — px is an absolute unit, entirely disconnected
from that preference, as covered in the Colors, Units & Typography module. */html { font-size: 100%; } /* respects the browser/OS default, typically 16px */
h1 { font-size: 2rem; } /* 2 * the root font-size — scales if the user changes it */Forgetting the viewport meta tag, then "fixing" it with media queries alone
<!-- missing from <head>: -->
<!-- <meta name="viewport" content="width=device-width, initial-scale=1"> -->
<style>
@media (max-width: 600px) { .nav { display: none; } }
</style>
/* Without the viewport tag, a phone renders the page at a virtual desktop
width (often 980px) and scales the whole thing down — the media query
never even fires as "mobile," exactly the trap covered in the
Responsive Design & Media Queries module. */Writing desktop-first media queries in a mobile-first project (or the reverse)
.sidebar { display: none; }
@media (min-width: 768px) { .sidebar { display: block; } }
@media (max-width: 900px) { .sidebar { display: none; } }
/* At exactly 768–900px, both queries are active — the LAST one in source
order wins, which may not be the intended layout, and is genuinely
hard to reason about at a glance. */The Mobile-First Design Principles module's fix is procedural, not just technical: pick one direction — almost always min-width, mobile styles as the unprefixed default, each breakpoint adding rules as the screen grows — and never mix max-width queries into the same project.
Small Habits That Compound Into a Codebase People Actually Want to Touch
None of the following is individually dramatic — each is a small, consistently-applied habit that, multiplied across a real codebase with dozens of contributors over years, is the actual difference between a stylesheet that stays healthy and one that gets rewritten from scratch.
Prefer shorthand only when you mean every value it sets
.card { border: 1px solid #ccc; border-radius: 8px; }
/* Later, another rule tries to "just" change the color */
.card--highlighted { border: 2px solid gold; }
/* This silently changes border-WIDTH too (1px -> 2px), and if the
original rule had also set border-style differently, that would be
reset as well — shorthand always sets every value it covers, even
the ones you didn't mean to touch. */.card { border: 1px solid #ccc; border-radius: 8px; }
.card--highlighted { border-color: gold; }Comment the "why," not the "what"
/* Set the display to flex */
.row { display: flex; }/* z-index 999 needed to sit above the third-party chat widget,
which injects its own container at z-index 998 */
.modal { z-index: 999; }Delete dead CSS deliberately, don't just accumulate it
Unlike JavaScript, an unused CSS rule produces no error, no warning, and no test failure — it just sits in the file forever, adding to the mental load of every future reader who has to figure out whether it is safe to remove. Deleting a class from CSS the same day it is removed from the markup (not "sometime later") keeps this debt from accumulating silently.
Cmd/Ctrl+Shift+P → Show Coverage) will highlight CSS rules that were never applied while the page was loaded and interacted with — a genuinely useful periodic check on a large, long-lived stylesheet, though it should inform a cleanup, not replace actually understanding why a rule exists before deleting it.A CSS Audit at a Retail Platform in Minneapolis
A mid-size e-commerce team schedules a "CSS health" sprint after a new hire spends most of their first two weeks just trying to safely change a product card's border radius — every attempt broke something else on a different page. The team pulls the full compiled stylesheet for review and finds !important on 340 separate rules, 60+ distinct spacing values that were never meant to be different from each other, and three different naming conventions layered on top of each other from three different eras of the codebase.
.pdp-container .product-info .price-block .price.sale-price.discounted {
color: #d32f2f !important;
margin-top: 13px !important;
}What the audit traces this back to
Every issue maps directly back to habits covered in this module: the deeply chained, overqualified selector (Part 05) meant nobody could safely write a competing rule without an even longer selector or an !important — which is exactly how the !important count reached 340 in the first place (Part 03). The margin-top: 13px is one of dozens of near-duplicate spacing values with no shared system behind them (Part 02). And the class itself mixes three different naming styles depending on which era of the product it came from (Part 01).
.price--sale {
color: var(--color-danger);
margin-top: var(--space-3);
}The sprint's actual output was not a redesign — the page looks identical to a user. It was a systematic pass converting the highest-traffic components to a spacing scale, a consistent BEM naming scheme, and flat, class-only selectors with zero !important in the new rules. Three months later, the team's internal metric for "average time to safely ship a small CSS change" had dropped by more than half — not because anyone got faster at writing CSS, but because the CSS itself stopped fighting back.
Five Misconceptions About Writing Maintainable CSS
6 Interview Questions — With Complete Answers
Broken CSS, Fixed — Five More Patterns Worth Recognizing on Sight
Relying on !important to fix a specificity fight instead of the selector
.sidebar .widget .title { font-size: 14px; }
.title { font-size: 18px !important; }.widget__title { font-size: 18px; }Hardcoding a color instead of referencing a design token
.button-primary { background: #1a73e8; }
.link { color: #1a73e8; }
.badge--info { border-color: #1a73e8; }:root { --color-primary: #1a73e8; }
.button-primary { background: var(--color-primary); }
.link { color: var(--color-primary); }
.badge--info { border-color: var(--color-primary); }Styling based on an element's position instead of a semantic class
.list-item:nth-child(3) { font-weight: bold; }
/* Meant to bold "the featured item" — but that just happens to be
the 3rd item today. Reordering the list silently moves the styling
to the wrong item. */.list-item--featured { font-weight: bold; }
/* <li class="list-item list-item--featured">...</li> */Forgetting that CSS custom properties inherit and cascade like any other property
:root { --card-bg: white; }
.dark-mode .sidebar { --card-bg: #1a1a1a; }
.card { background: var(--card-bg); }
/* If .card lives OUTSIDE .sidebar in the DOM, this override never
applies to it — custom properties only cascade to descendants of
whatever selector sets them. */:root { --card-bg: white; }
.dark-mode { --card-bg: #1a1a1a; }
.card { background: var(--card-bg); }Duplicating an entire rule for one small variation
.card { padding: 16px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.1); background: white; }
.card-compact { padding: 8px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.1); background: white; }.card { padding: 16px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.1); background: white; }
.card--compact { padding: 8px; }Warnings and Symptoms You Will Hit — And Exactly Why
🎯 Key Takeaways
- ✓Pick one naming convention (BEM or similar) and apply it everywhere — the specific system matters less than consistency, which makes class names predictable, searchable, and self-documenting.
- ✓Magic numbers (ad hoc pixel values with no shared system) are one of the biggest sources of layout drift. A spacing scale built from CSS custom properties turns "make spacing tighter everywhere" into a one-line change instead of a codebase-wide search.
- ✓Specificity wars escalate quietly — each locally reasonable fix (a more specific selector, then !important) raises the bar for the next fix. Flat, single-class selectors and one class per intended appearance avoid the escalation entirely.
- ✓!important should be rare, reserved mainly for overriding third-party CSS you cannot beat on specificity — not a routine tool for winning disagreements inside your own stylesheet.
- ✓box-sizing: border-box, applied globally, prevents the most common box-model surprise: padding and border silently expanding an element beyond its declared width.
- ✓Splitting CSS into files only helps if load order stays predictable and each file has a clear, guessable responsibility — organization and selector discipline are two separate problems.
- ✓Small habits compound: minimal shorthand usage, comments that explain "why" not "what," and deleting dead CSS as soon as its markup is removed are all individually minor but collectively define whether a stylesheet stays healthy for years or gets rewritten from scratch.
What comes next
The final module in this track pulls everything together into interview-ready form — semantic HTML, the box model, specificity, Flexbox vs Grid, responsive design, and accessibility, plus fully worked hands-on layout challenges like centering a div three different ways and fixing a broken sticky footer.
Module 42 → HTML & CSS Interview Prep — Common Questions and PatternsDiscussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.