The Second Month: Styles That Don't Rot
CSS is easy for three weeks and unbearable by month three. Not because people write it badly — because nothing in the language stops a stylesheet from becoming a place where deletion is a gamble.
Week one: a couple of hundred lines, everything obvious.
Month three: four thousand lines. Nobody dares delete anything, because there's no way to know what a rule affects. Every new change needs one more selector than the last, and half of them end in !important. Somewhere in there are fourteen shades of grey, nine spacing values between 12 and 20 pixels, and three definitions of "the card."
Nothing careless happened. Each individual change was reasonable. The problem is structural, and it has two causes worth naming precisely:
CSS is global. Every rule you write applies to the whole document. A class name written in one file will style a matching element in a file you've never opened.
Deletion is unsafe. Nothing tells you whether a rule is still used. So nobody deletes, everybody adds, and the file only grows.
CSS doesn't rot because people write it badly. It rots because nothing stops it: every rule is global, and every deletion is a gamble.
Everything below is a way of imposing from outside the two things the language doesn't give you: scope and consistency.
Tokens: decide once, use everywhere
The fourteen greys are the visible symptom. The cause is that every colour was chosen at the moment it was needed, by whoever needed it, and there was no answer to "which grey is our grey."
Custom properties fix this by making the decision once and giving it a name. A small set of values, declared at the root, referenced everywhere: colours, a spacing scale, a type scale, radii, shadows, transition durations.
The rule that makes it work is stricter than people expect: inside a component, no raw values. Not a hex code, not a pixel number that isn't from a scale. If the value you need doesn't exist, that's a decision to make in the token file — deliberately, once — not a number to type inline.
Two scales carry most of the weight:
Spacing. A geometric-ish sequence — 4, 8, 12, 16, 24, 32, 48, 64 — and nothing between. The exact numbers matter far less than the fact that there are eight of them instead of forty. Interfaces look "off" for a reason that's hard to see and easy to fix: a 13-pixel gap next to a 16 and an 18 reads as sloppiness even to someone who can't name it.
Type. Five or six sizes with matching line heights, and every piece of text on the site uses one of them.
This is also the exact point where design and code meet. The UX sphere's design-systems article describes the same object from the other side: the tokens are the contract between the two disciplines. When a designer's spacing scale and your CSS variables are the same eight numbers, a whole category of argument disappears.
Scope: three honest answers
The global problem has three real solutions in use today. All three work. What doesn't work is having none, or having all three in different folders.
A naming convention — BEM and its relatives. You give every class a name that encodes its component and role, so card__title and card--featured collide with nothing by construction. It needs no tooling and works anywhere, which is why it's still everywhere. The cost is verbosity and discipline: nothing enforces it but you.
Scoped styles — CSS Modules, single-file components, scoped attributes. The build tool rewrites your class names to be unique per file, so collisions become impossible rather than merely unlikely. The mental model stays plain CSS. This is the default in most component frameworks now, and it's the easiest to recommend if you're using one.
Utility classes — Tailwind and similar. You stop writing stylesheets and compose from a fixed vocabulary of tiny classes directly in the markup. Scope stops being a problem because nothing is ever named; consistency is enforced because the utilities are the scale. The costs are real and worth stating: the markup gets noisy, and repeated patterns must be extracted into components or you'll copy fifteen classes by hand forever. It's a genuine trade, not a fashion — and the reason it grew so fast is that it makes the two hard problems in this article structurally impossible.
Pick one per project. The unmaintainable codebase is almost always the one with three.
Components own their styles
Whatever you picked, one rule does more for maintainability than the rest combined: a component styles itself and nothing else.
Concretely: no selector inside a component reaches out to change something in another component. No .sidebar .button overriding what a button looks like from the outside. If a button needs to look different in the sidebar, that's a variant of the button, decided by the button, exposed as a class or a prop.
Layout is the exception that proves it, and it has a clean solution: a component decides its own internals; its parent decides where it sits and how much space is around it. So margins belong to the layout, not to the component — a card with a built-in bottom margin will be wrong in the first context that doesn't want one.
Keep nesting shallow, no more than two or three levels. Native CSS nesting exists now and is pleasant, but it's the same specificity trap in nicer clothes: five levels of nesting produces a selector nothing can override politely.
Layers, in one order
A stylesheet stays comprehensible when it has an order that everyone knows:
- Reset / base — normalising defaults,
box-sizing, base typography, focus styles. - Tokens — the variables.
- Layouts — page skeletons and reusable arrangements.
- Components — the bulk.
- Utilities — a small set of single-purpose helpers, if you use any.
If you're on a modern project, @layer makes this order explicit and enforceable: a later layer wins regardless of specificity, so a component can override a base style without a specificity fight. That single feature removes most of the reason !important ever gets typed.
The delete test
Here's a check worth running on any codebase, including yours: pick a class you believe is unused and delete it. Can you be confident, in under a minute, that nothing broke?
If yes, your architecture is working. If you have to grep the whole project, click through six pages and still feel uneasy, you've found the actual disease — and the treatments are exactly the ones above: scoping so a name has one home, and colocation so styles live next to the markup that uses them.
This is also the honest measure of whether you understand your own stylesheet, and it's a nastier test than it looks. Code that works today feels understood; the How to Learn sphere would call that a textbook illusion of competence — fluency mistaken for knowledge. The real test isn't writing it. It's changing it in three months, when the feeling of understanding has evaporated and only the structure is left.
In practice
Write the tokens before the first component. Twenty minutes, up front: colours, eight spacing values, six type sizes, two radii. Everything after that is a choice from a menu instead of a decision from scratch.
Ban raw values in component styles for one project. Every time you want an arbitrary number, go and look at whether the scale already has one. It nearly always does, and the habit installs itself in a week.
Count your greys. Search your CSS for hex codes and list the unique ones. The number is usually shocking, and it's the fastest possible demonstration of why tokens exist.
Read your own stylesheet from three months ago. Not to fix it — to notice which parts you can't reconstruct. Those parts are the feedback; they tell you what your naming was actually failing to say.
Try the delete test once a month. It's a two-minute health check on the thing that decides whether the next six months are pleasant.
Choose one convention and write it down in the project's readme, in three lines. Half of CSS rot is two people each following a different sensible system.
Check yourself
Close the article and answer in your own words:
- What are the two structural properties of CSS that make stylesheets rot?
- What does a token buy you beyond saving a few keystrokes?
- Why does a spacing scale of eight values look better than free choice?
- Name the three approaches to scoping and one honest cost of each.
- Why should margins belong to the layout rather than to the component?
- What does
@layerchange about overriding, and what does it replace? - What is the delete test, and what does failing it tell you?
In short
- CSS rots for two structural reasons: every rule is global, and nothing tells you whether a rule is still needed.
- Tokens make each visual decision once and give it a name; inside components, no raw values.
- A spacing scale and a type scale carry most of the visual consistency, and they're also the contract with design.
- Three working answers to scope — naming conventions, scoped styles, utility classes. Pick one per project; the mess is having three.
- A component styles itself and nothing else; the parent decides position and spacing around it.
- Keep nesting to two or three levels; native nesting is the same specificity trap in nicer clothes.
- Order the stylesheet as reset, tokens, layouts, components, utilities — and let
@layerenforce it instead of!important. - The delete test measures the architecture, and rereading your own old CSS measures whether you understood it or merely wrote it.