#Why CSS gets hard to maintain
AddedCSS is easy to write and hard to keep tidy, and the reason is structural, not a matter of discipline. Every selector in every stylesheet lives in one global namespace and competes in one cascade. A rule written for the checkout page can style a heading on the blog, because nothing in the language ties .title to the file or the component it was written for. When two rules match the same element, the winner is decided by origin, layers, specificity and then source order (see The cascade), not by which team meant it.
A useful mental model is a shared office whiteboard with no names on anything. Anyone can write on it, nobody can tell who wrote what, and erasing something might break a meeting you have never heard of. So people only ever add: a more specific selector to beat the last one, an !important to beat that, and old rules nobody dares delete. Stylesheets grow append-only, and specificity climbs.
Every approach in this chapter attacks one or more of these forces: naming conventions (BEM) and generated class names (CSS Modules, CSS-in-JS) fight the global namespace; utility classes (Tailwind) avoid writing new selectors at all; cascade layers and @scope give the cascade structure; tooling (Sass, PostCSS) makes the source easier to organise and to ship. The demo shows the forces in miniature.
/* checkout.css — written first */
.title { color: rgb(29, 78, 216); font-size: 18px; margin: 0 0 8px; }
/* blog.css — another team, loaded later */
.title { color: rgb(185, 28, 28); }
/* "fix" for the sidebar */
#sidebar .title { color: rgb(21, 128, 61); }
/* "fix" for the fix */
.promo .title { color: rgb(126, 34, 206) !important; }<section class="checkout">
<h2 class="title">Order summary</h2>
</section>
<aside id="sidebar">
<h2 class="title">Related posts</h2>
<div class="promo">
<h2 class="title">Sale</h2>
</div>
</aside>- Both stylesheets define
.title, with the same specificity (0,1,0). The checkout team's "Order summary" was meant to be blue, butblog.csscomes later in the source, so for thecolorproperty the later rule wins: it rendersrgb(185, 28, 28), the blog's red. Nobody on the checkout team changed anything. - The leak goes the other way too:
font-size: 18pxand the margin were only incheckout.css, and the blog rule didn't set them, so all three headings, including the sidebar ones, measure18px. Rules merge property by property, so a "harmless" declaration travels to every.titleon the site. - Someone needs the sidebar title green and writes
#sidebar .title. That's specificity (1,1,0), which beats both class rules, so "Related posts" isrgb(21, 128, 61). It works, and it means any later rule for titles in the sidebar now needs an id too. - The promo inside the sidebar needs purple.
.promo .titleis (0,2,0), which loses to the id rule, so the author reaches for!importantand the "Sale" heading isrgb(126, 34, 206). The only way to override that later is another!importantwith higher specificity, or a later one. - This is the whole maintenance problem in four rules: a global name collided, source order silently decided a winner, and a specificity war started. In a real app the order of
checkout.cssandblog.cssis often decided by the bundler, which makes step 1 change between builds.
The forces, and what fights each one:
| Force | What goes wrong | What fixes it |
|---|---|---|
| Global namespace | Two .title rules from different files style each other's elements | Naming conventions (BEM), generated names (CSS Modules, CSS-in-JS), @scope |
| Specificity escalation | Ids, long chains and !important to beat earlier rules | Flat single-class selectors, :where() for zero specificity, cascade layers |
| Source-order dependence | The winner changes when the bundler reorders CSS chunks | Layers (order declared once), scoped names that never collide |
| Append-only growth | Nobody knows which rules are unused, so nothing is deleted | Styles co-located with components (delete the component, delete its CSS), utilities generated from what's used (Tailwind) |
| Coupling to markup | .sidebar > div > ul li a breaks when the HTML changes | Classes on the element itself instead of structural chains |
| Inheritance surprises | color, font and line-height flow into components from their parents | Components set the inherited properties they rely on; resets set a known baseline (Resets) |
Using the demo's four rules, suppose the bundler now loads blog.css before checkout.css. What colour is each heading?
Show answer
"Order summary" turns blue, the two sidebar headings don't change (green and purple).
- The two
.titlerules still have equal specificity (0,1,0), so source order decides. Nowcheckout.csscomes last, so itscolor: rgb(29, 78, 216)wins for every.titlethat nothing stronger targets. - "Related posts" is still matched by
#sidebar .title(1,1,0), which beats any single class regardless of order: green. - "Sale" still has an
!importantdeclaration. Important declarations beat all normal ones whatever their specificity or order: purple. - So only the element whose winner was decided by order changed. That's why order-dependent CSS is fragile: the same files produce different pages when they are concatenated differently.