Organising CSS
A stylesheet for one page is easy to manage. A stylesheet for a whole website, edited by several people over two years, is not. Rules start fighting each other, nobody dares to delete anything, and every fix needs !important. The problem is rarely the CSS language itself. It is how the CSS is organised.
This lesson shows a practical way to organise CSS: a simple file structure, design tokens, a naming method called BEM, small utility classes, cascade layers to control the order, and a few habits that keep specificity low.
Why CSS gets messy
CSS has a few features that cause trouble in big projects:
- Everything is global. A rule for
.titleaffects every element with that class, on every page. - Specificity wars. One developer writes
#sidebar .title, the next needs#sidebar .widget .titleto override it, and the selectors keep growing. - Order matters. When two rules have the same specificity, the later one wins, so moving a file can change the design.
- Dead code. It is hard to know whether a rule is still used, so old CSS is never deleted.
A good structure answers three questions: where does this rule belong, what is it called, and what should it override?
A simple file structure
Split your CSS by job, from the most general to the most specific:
css/
tokens.css colours, spacing, fonts as custom properties
reset.css small fixes to browser defaults
base.css plain elements: body, headings, links, images
layout.css page-level layout: header, grids, wrapper
components/
button.css
card.css
navbar.css
utilities.css small single-purpose helpersEach component gets its own file, named after the component. When you need to change the card, you know exactly where to look, and when a component is removed, you delete its file.
For the live site, these files are usually combined into one file by a build tool, because each separate file means another download. If you load them with CSS @import, the browser has to download them one after another, which is slow; see Best Practices & Performance.
Design tokens
Design tokens are the small, named decisions your design is built from: colours, spacing steps, font sizes, corner radius. Store them once as custom properties (see Custom Properties) and use them everywhere:
:root {
/* colours, named by role */
--color-brand: #0f766e;
--color-text: #1e293b;
--color-surface: #f8fafc;
--color-danger: #b91c1c;
/* a spacing scale */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 1rem;
--space-4: 1.5rem;
--space-5: 2.5rem;
/* shape and type */
--radius: 0.5rem;
--font-body: system-ui, sans-serif;
}Tokens stop the "47 slightly different greys" problem. A designer can change the brand colour or make the whole site more spacious by editing one file, and dark mode becomes a matter of giving the colour tokens new values, as in Dark Mode & Theming.
Naming classes with BEM
BEM stands for Block, Element, Modifier. It is a naming method that makes it clear which component a class belongs to:
- A block is a standalone component:
.card,.navbar,.btn. - An element is a part of a block, joined with two underscores:
.card__title,.card__image. - A modifier is a variation, joined with two dashes:
.card--featured,.btn--small.
<article class="card card--featured">
<img class="card__image" src="lassi.jpg" alt="A glass of mango lassi">
<h2 class="card__title">Mango lassi</h2>
<p class="card__price">₹80</p>
</article>The class names are long, but they bring real benefits. Every selector is a single class, so specificity stays flat and predictable. Reading the HTML, you can see which CSS file styles each element. And .card__title will never accidentally style a title in the navbar.
Name things by purpose, not appearance. .btn--danger still makes sense after the designer changes the colour from red to orange; .btn--red does not.
Utility classes
A utility class does one small job and can be used anywhere, such as .text-center or .visually-hidden. A handful of utilities saves repeating the same declarations in many components. A popular example is a "flow" utility that puts equal space between stacked children:
.flow > * + * {
margin-block-start: var(--flow-space, 1em);
}* + * means "every child that comes after another child", so the first child gets no top margin and the rest get even spacing. A component can change the gap by setting --flow-space.
Some teams go further and build almost everything from utilities, using frameworks such as Tailwind CSS. That is a valid approach with its own trade-offs. For learning, a mix works well: components for the main pieces, a few utilities for common small jobs.
Order the cascade with layers
Cascade Layers let you decide, in one line, which group of styles wins. Declare the layer order at the very top, and later layers beat earlier ones no matter how specific the selectors inside them are:
@layer reset, base, layout, components, utilities;
@layer base {
a { color: var(--color-brand); }
}
@layer components {
.navbar__link { color: white; }
}
@layer utilities {
.text-center { text-align: center; }
}This matches the file structure above: a utility always beats a component, and a component always beats base styles. You can also load whole files into a layer with @import url("card.css") layer(components);.
The next example puts tokens, layers, BEM and a utility together in one page.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Organised CSS</title>
<style>
@layer reset, base, components, utilities;
:root {
--color-brand: #0f766e;
--color-text: #1e293b;
--color-surface: #f1f5f9;
--space-2: 0.5rem;
--space-3: 1rem;
--radius: 0.5rem;
}
@layer reset {
* { box-sizing: border-box; }
body { margin: 0; }
}
@layer base {
body { padding: var(--space-3); font-family: system-ui, sans-serif; color: var(--color-text); }
h2 { margin: 0; }
}
@layer components {
.card {
max-width: 300px;
padding: var(--space-3);
border-radius: var(--radius);
background: var(--color-surface);
}
.card--featured { outline: 3px solid var(--color-brand); }
.card__price { margin: 0; font-weight: bold; color: var(--color-brand); }
.card__button {
padding: var(--space-2) var(--space-3);
border: 0;
border-radius: var(--radius);
background: var(--btn-bg, var(--color-brand));
color: white;
font: inherit;
}
.card__button--danger { --btn-bg: #b91c1c; }
}
@layer utilities {
.flow > * + * { margin-block-start: var(--flow-space, 0.75rem); }
.text-center { text-align: center; }
}
</style>
</head>
<body>
<article class="card card--featured flow">
<h2 class="card__title">Mango lassi</h2>
<p class="card__price">₹80</p>
<button class="card__button" type="button">Add to order</button>
</article>
<article class="card flow text-center" style="--flow-space: 0.25rem; margin-top: 1rem">
<h2 class="card__title">Masala chai</h2>
<p class="card__price">₹30</p>
<button class="card__button card__button--danger" type="button">Remove</button>
</article>
</body>
</html>Look at how the button variants work. The button's background is var(--btn-bg, var(--color-brand)): use --btn-bg if it is set, otherwise the brand colour. The danger modifier only sets --btn-bg. Configuring components through custom properties like this keeps modifiers tiny and avoids repeating whole declarations.
Keep specificity low
Most CSS headaches come from specificity creeping upwards. A few habits keep it flat:
- Style with classes, not IDs. One ID outweighs any number of classes.
- Avoid long descendant chains such as
.page .main .card h2. Give the element its own class instead. - Keep CSS Nesting shallow.
- Use
:where()for default styles that should be easy to override. - Treat
!importantas a last resort. If you need it often, the structure is wrong.
Common mistakes
- Naming by looks.
.big-blue-textbecomes a lie the day the design changes. Name by purpose. - One giant file. A 5,000-line
style.csswith no order makes every change risky. Split by job and component. - Fixing with
!important. It wins today and starts a war tomorrow. Find out why the rule loses and fix the cause. - Never deleting CSS. When you remove a component from the HTML, remove its CSS file too. Browser DevTools has a Coverage panel that shows unused CSS.
Key points
- Organise CSS from general to specific: tokens, reset, base, layout, components, utilities.
- Store colours, spacing and other design decisions as custom property tokens with role-based names.
- BEM names (
.block__element--modifier) keep selectors to a single class and show which component owns each style. - A few utility classes, such as a flow spacer, save repetition; name everything by purpose.
- Declare cascade layers in order so utilities beat components and components beat base styles.
- Keep specificity low: classes over IDs, short selectors, shallow nesting and almost no
!important.
Exercise
You are given this CSS for a school website: #main .content .notice h3 { color: red; } and .notice-title { color: green !important; }. Rewrite it in an organised way: create a --color-alert token, use BEM classes for a notice block with a title element and an urgent modifier, and put the rules into base and components layers. Write both the CSS and a small HTML snippet that uses it.
Show solution
@layer base, components;
:root {
--color-alert: #b91c1c;
--color-text: #1e293b;
}
@layer base {
h3 { color: var(--color-text); }
}
@layer components {
.notice { padding: 1rem; border-left: 4px solid #cbd5e1; }
.notice__title { margin: 0; }
.notice--urgent { border-left-color: var(--color-alert); }
.notice--urgent .notice__title { color: var(--color-alert); }
}<section class="notice notice--urgent">
<h3 class="notice__title">School closed on Monday</h3>
<p>Due to heavy rain in Lucknow, classes resume on Tuesday.</p>
</section>The ID chain and !important are gone. Every selector is one or two classes, the colour lives in a token, and the components layer always wins over the plain h3 rule in base.
Quick Quiz
1. In BEM, what does .card__title represent?
Two underscores join a block and one of its elements; two dashes would mark a modifier.
2. Why are design tokens useful?
Tokens give every colour, space and size a single source of truth.
3. With @layer reset, base, components, utilities;, which layer wins a conflict?
Layers declared later win over earlier layers, regardless of selector specificity inside them.
4. Which class name follows the advice to name by purpose?
A purpose-based name stays correct even if the colour or size changes later.
Learn CSS with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
