You’ve made it through a lot of CSS by now — the box model, Flexbox, Grid, custom properties, animations, the works. You can build basically any layout you can picture. So this article isn’t about a new property. It’s about a different skill entirely: writing CSS that’s still pleasant to work with three months and three thousand lines later.
Here’s the uncomfortable truth about CSS — it’s deceptively easy to write and surprisingly hard to maintain. Anyone can make one button look right. The challenge is a whole codebase where changing one color doesn’t break four pages, where you can find the rule you need without grepping in despair, and where a teammate can add a feature without being terrified of touching the stylesheet. That’s what we’re after here. None of this is fancy. It’s just a set of habits that separate stylesheets that age well from ones that rot.
Why CSS rots (and what we’re fixing)
CSS has two features that make it powerful and also make it dangerous: the cascade and global scope. Every selector you write is global — there’s no built-in boundary stopping .title in one file from clobbering .title in another. And the cascade means specificity wars, where you pile on more selectors and !important just to win a fight you started yourself.
Left unchecked, this produces the classic legacy stylesheet: thousands of lines nobody dares delete, because nobody’s sure what still depends on them. Every best practice below is, at its core, a way to tame those two features — to add the structure and boundaries CSS doesn’t give you for free.
Best practice means 'fewer surprises later'
A best practice isn’t a rule you follow to please a linter. It’s a small decision now that saves you from a debugging session later. When you read these, don’t memorize them as commandments — understand the failure they prevent. That’s what makes them stick.
Name things so the name tells you what it is
Class names are the vocabulary of your stylesheet, and good names do a lot of quiet work. The goal is a name that describes what the element is, not what it looks like.
/* Avoid — names tied to appearance */
.red-text { color: crimson; }
.big-margin-box { margin: 48px; }
/* Better — names tied to meaning */
.error-message { color: crimson; }
.section-spacing { margin: 48px; }
Why does this matter? Because appearance changes. The day your error messages go from red to orange, a class called .red-text becomes a lie — it says red, but it’s orange now. A class called .error-message still makes perfect sense. Name by role, and your CSS survives redesigns.
For multi-word names, pick one casing and stick to it. kebab-case (lowercase with hyphens) is the CSS convention and reads cleanly:
.product-card { /* ... */ }
.product-card-title { /* ... */ }
.nav-link-active { /* ... */ }
A word on BEM
As projects grow, a naming convention pays for itself. The most popular is BEM — Block, Element, Modifier. The idea is to encode an element’s relationship right in its name:
/* Block: a standalone component */
.card { /* ... */ }
/* Element: a part of the block, joined with __ */
.card__title { /* ... */ }
.card__image { /* ... */ }
/* Modifier: a variant of the block, joined with -- */
.card--featured { /* ... */ }
You don’t have to use BEM specifically. The point is to have some consistent system so that when you see a class name, you immediately know whether it’s a component, a piece of one, or a variant. Consistency beats cleverness here every time.
Keep specificity low and flat
Specificity is the scoring system the browser uses to decide which rule wins when two rules target the same element. The more specific a selector, the harder it is to override later — and that’s the trap. Every time you write a deeply nested selector to win a battle, you’ve made the next override even harder.
/* Avoid — high specificity, hard to override */
#sidebar ul li a.active { color: blue; }
/* Better — a single, low-specificity class */
.sidebar-link-active { color: blue; }
The first rule is a fortress. To override it later you’ll need something even more specific, and the arms race begins. The second is flat and easy: one class, easy to beat with another class when you need to. Aim to style with single class selectors as much as you can. Avoid IDs for styling (they’re far too specific), avoid deep nesting, and treat !important as a last resort — because once you use it, the only thing that can override it is another !important, and now you’re stuck.
!important is borrowing against your future
Reaching for !important feels like winning. It isn’t — it’s borrowing. It works today, but the next person (often you, next month) who needs to change that style finds the normal rules don’t apply, has to figure out why, and ends up adding another !important. A stylesheet full of them is one where the cascade no longer means anything. Use it only for genuine overrides you can’t reach any other way, like overriding a third-party widget’s inline styles.
If you want the full mechanics of how this scoring works, the CSS specificity article breaks it down in detail. Understanding it is exactly what lets you keep it low on purpose.
Don’t repeat yourself — use custom properties
The same color hex code copy-pasted into forty rules is forty places to update when the brand color changes, and you will miss one. CSS custom properties (variables) fix this by giving every shared value a single source of truth:
:root {
--color-brand: #00b8e6;
--color-text: #1a2733;
--space-md: 16px;
--radius: 8px;
}
.button {
background: var(--color-brand);
padding: var(--space-md);
border-radius: var(--radius);
}
.card {
color: var(--color-text);
padding: var(--space-md);
border-radius: var(--radius);
}
Now your brand color, your spacing, and your corner radius each live in exactly one spot. Change --color-brand once and every button, link, and accent updates together. This is the single highest-leverage habit in modern CSS — it turns a redesign from a find-and-replace nightmare into editing a handful of lines at the top of a file. If custom properties are new to you, the CSS variables article goes deeper, and you can even scaffold a starter palette with a CSS variables generator if you want a quick head start.
A close cousin of this rule: define your scale, not arbitrary one-off values. Instead of reaching for 13px, 15px, 19px, 23px wherever a number feels right, pick a spacing and type scale (say multiples of 4px) and use variables for them. Consistent rhythm is a huge part of why professional sites feel professional.
Organize the file so future-you can find things
A stylesheet is a document, and documents need structure. When everything’s dumped in one flat file in the order you happened to write it, finding the rule you need becomes a scavenger hunt. A little organization goes a long way.
A reliable order to put things in:
- Variables and resets — your
:rootcustom properties and any base resets first. - Base elements — bare-tag styles like
body,a,h1–h6,p. - Layout — the big structural pieces: header, footer, main grid, containers.
- Components — buttons, cards, forms, navigation, the reusable bits.
- Utilities — small single-purpose helpers last.
Group related rules together and label the groups with comments so each section announces itself:
/* ============================================
COMPONENTS — Buttons
============================================ */
.button { /* ... */ }
.button--primary { /* ... */ }
.button--ghost { /* ... */ }
On a real project you’d split these into separate files (one per component) and combine them with a build step or @import. But even in a single file, the principle holds: a predictable order means you always know roughly where to look. You’re writing for the person who has to read this next, and that person is usually you, tired, six months from now.
Build mobile-first and lean on the cascade
When you write your base styles for small screens first and add complexity for larger ones with min-width media queries, your CSS tends to come out simpler and your defaults tend to be the safer, more robust ones. We covered the full reasoning in the responsive design article, but as a maintainability habit it’s worth restating: mobile-first stylesheets are usually shorter and have fewer overrides fighting each other.
More broadly, work with the cascade instead of against it. Set sensible defaults high up — a base font-family and color on body, for instance — and let them flow down to everything. Then only override where something genuinely differs. Most beginners over-specify, restating values that would have been inherited for free. Letting inheritance do its job means less code and fewer places for things to drift out of sync.
/* Set defaults once, high up — children inherit */
body {
font-family: system-ui, sans-serif;
color: var(--color-text);
line-height: 1.6;
}
/* Only override where it actually differs */
code {
font-family: ui-monospace, monospace;
}
Comment the why, not the what
Good CSS comments don’t narrate the obvious — anyone can read that color: red sets the color red. They explain the things the code can’t say: a magic number, a workaround for a browser quirk, a deliberate non-obvious choice.
.modal {
/* 60px clears the fixed header so the modal isn't hidden behind it */
margin-top: 60px;
/* z-index 1000 keeps this above the sticky nav (z-index 100) */
z-index: 1000;
}
Six months from now, that 60px looks completely arbitrary without the note. The comment turns “why on earth is this here?” into “ah, of course.” Comment the surprises, the hacks, and the decisions a future reader might be tempted to “clean up” and accidentally break.
Format consistently and validate
Pick a formatting style and apply it everywhere — one declaration per line, a space after the colon, consistent indentation. You don’t have to do this by hand; a formatter does it instantly and identically every time, which means diffs stay clean and code reviews focus on real changes instead of whitespace. A CSS formatter is handy for tidying up messy code in one pass, and running your stylesheet through a CSS validator catches typos, invalid properties, and unsupported values before they ship as silent bugs.
Let tools handle the boring parts
Formatting and validation are exactly the kind of mechanical work you should never do manually. Automate them and free your attention for the decisions that actually require judgment — naming, structure, and what the design should be. Consistency you don’t have to think about is the best kind.
A quick checklist of habits
Pulling the threads together, here’s the practical shortlist to keep in mind every time you write CSS:
- Name by meaning, not appearance (
.error-message, not.red-text), in consistentkebab-case. - Adopt a naming convention like BEM so names reveal structure.
- Keep specificity low and flat — single classes, no IDs for styling, no deep nesting.
- Treat
!importantas a genuine last resort. - Put every shared value in a custom property so there’s one source of truth.
- Use a consistent scale for spacing and type rather than arbitrary numbers.
- Organize the file: variables, base, layout, components, utilities — with comment headers.
- Write mobile-first and lean on inheritance and the cascade instead of over-specifying.
- Comment the why — magic numbers, hacks, deliberate choices.
- Format and validate with tools, not by hand.
Wrapping up
CSS best practices aren’t a separate, advanced topic bolted onto the language — they’re just the difference between knowing the properties and using them well. Every habit here comes down to the same idea: CSS gives you enormous freedom and almost no guardrails, so you add the structure yourself. Meaningful names, low specificity, single sources of truth, a predictable file layout, and a little discipline about comments and tooling — that’s the whole game.
You don’t need to apply all of this perfectly from day one. Pick a couple to start — naming by meaning and using custom properties are the two that pay off fastest — and let the rest become habit as you go. The reward is real: stylesheets you can come back to, hand off, and grow without dread. That’s what writing CSS well, as opposed to just writing CSS, actually means.