Architecture & Best Practices
Writing CSS that works is the first step. Writing CSS that stays maintainable as your project grows is the harder challenge. This chapter covers naming conventions, file organisation, and architectural patterns used in professional projects.
The problem
CSS has no built-in scoping mechanism. Every rule is global. On a small project, this is fine. On a large project, it leads to:
- Naming conflicts -- two developers independently create a
.titleclass with different styles - Specificity wars -- selectors grow longer to override other rules
- Dead CSS -- nobody removes old rules because they are afraid of breaking something
- Unpredictable side effects -- changing one rule breaks something on a different page
Architecture conventions solve these problems.
BEM naming convention
BEM stands for Block, Element, Modifier. It is the most widely adopted CSS naming convention.
Structure
.block {}
.block__element {}
.block--modifier {}
| Part | What it represents | Example |
|---|---|---|
| Block | A standalone component | .card |
| Element | A part of a block that has no meaning on its own | .card__title |
| Modifier | A variation of a block or element | .card--featured |
Example
<article class="card card--featured">
<img class="card__image" src="photo.jpg" alt="Photo" />
<div class="card__body">
<h3 class="card__title">Card Title</h3>
<p class="card__text">Description text here.</p>
<a class="card__link card__link--primary" href="#">Read more</a>
</div>
</article>
.card {
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
background-color: white;
}
.card--featured {
border-color: #4a90d9;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.card__image {
width: 100%;
height: 200px;
object-fit: cover;
}
.card__body {
padding: 20px;
}
.card__title {
margin: 0 0 8px;
font-size: 1.25rem;
}
.card__text {
margin: 0 0 16px;
color: #666;
}
.card__link {
text-decoration: none;
font-weight: 600;
}
.card__link--primary {
color: #4a90d9;
}
Why BEM works
- No nesting required -- every class is a single class selector (low, flat specificity)
- Self-documenting --
.card__titleclearly belongs to the.cardblock - No naming conflicts -- the block name acts as a namespace
- Easy to find -- search for
.cardto find all card-related CSS
BEM rules
- Never style bare elements inside a block -- use
.card__titleinstead of.card h3 - Keep blocks independent -- a block should not depend on being inside another block
- Do not nest elements --
.card__body__titleis wrong; use.card__title - Modifiers extend, not replace -- use both classes:
class="card card--featured"
File organisation
Single file
For small projects (a few pages), one styles.css file is fine. Organise it with comment sections:
/* ================================
Reset
================================ */
/* ================================
Base / Typography
================================ */
/* ================================
Layout
================================ */
/* ================================
Components
================================ */
/* ================================
Utilities
================================ */
Multi-file structure
For larger projects, split CSS into multiple files:
styles/
reset.css
tokens.css
base.css
layout.css
components/
card.css
button.css
nav.css
form.css
utilities.css
Import them in order in a main file or HTML:
@import "reset.css";
@import "tokens.css";
@import "base.css";
@import "layout.css";
@import "components/card.css";
@import "components/button.css";
@import "components/nav.css";
@import "components/form.css";
@import "utilities.css";
Note: In production, use a build tool to bundle these files.
@importcreates additional HTTP requests which slow page loads. Build tools like Vite, Webpack, or PostCSS combine them into a single file.
File ordering principle
The order matters. Load files from least specific to most specific:
- Reset / normalise -- remove browser defaults
- Tokens -- custom properties (colours, fonts, spacing)
- Base -- default styles for bare HTML elements
- Layout -- page structure (grid, sidebar, container)
- Components -- reusable UI components (card, button, nav)
- Utilities -- single-purpose overrides (
.hidden,.text-center)
Utilities come last because they must be able to override component styles.
CSS resets and normalisers
Browsers apply default styles to HTML elements. These defaults vary between browsers, causing inconsistencies.
CSS reset
A reset removes all default styles:
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
A more comprehensive version:
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: none;
text-size-adjust: none;
}
body {
min-height: 100vh;
line-height: 1.6;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
Normaliser
A normaliser (like Normalize.css) keeps useful defaults but makes them consistent across browsers. It is less aggressive than a reset.
Which to use?
| Approach | Best for |
|---|---|
| Reset | Full control; you define every style from scratch |
| Normaliser | Keeping sensible defaults; less CSS to write |
Most modern projects use a lightweight reset (like the one above) rather than a full normaliser.
Utility classes
Utility classes apply a single style:
.text-center { text-align: center; }
.text-right { text-align: right; }
.font-bold { font-weight: 700; }
.mt-0 { margin-top: 0; }
.mt-4 { margin-top: 16px; }
.mt-8 { margin-top: 32px; }
.hidden { display: none; }
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
Use utilities for one-off adjustments instead of creating new component classes:
<p class="card__text mt-4 text-center">Centred text with extra top margin.</p>
The .sr-only class is especially important -- it hides content visually but keeps it available to screen readers.
Utility-first CSS (Tailwind approach)
The utility-first approach (popularised by Tailwind CSS) composes entire designs from small utility classes:
<div class="p-6 max-w-sm mx-auto bg-white rounded-xl shadow-md flex items-center space-x-4">
<img class="h-12 w-12" src="logo.png" alt="Logo" />
<div>
<div class="text-xl font-medium text-black">Company Name</div>
<p class="text-slate-500">A great product.</p>
</div>
</div>
Utility-first CSS avoids naming problems entirely -- you never write class names at all. The trade-off is longer HTML and a different mental model.
Component-scoped styles
In modern frameworks (React, Vue, Svelte), CSS can be scoped to a component automatically:
- CSS Modules -- class names are made unique at build time (
.card_abc123) - Vue
<style scoped>-- adds a data attribute to scope selectors - Svelte -- scopes styles by default
- Shadow DOM -- fully encapsulated styles (Web Components)
These approaches solve the global scope problem at the framework level. If you are using a framework, consider them.
CSS preprocessors
Preprocessors like Sass and Less extend CSS with variables, nesting, mixins, and functions. They compile to standard CSS.
With the arrival of native CSS custom properties (chapter 13) and native nesting (chapter 15), the gap between preprocessors and plain CSS has narrowed significantly. Consider whether you still need a preprocessor.
| Feature | Native CSS | Sass |
|---|---|---|
| Variables | Yes (custom properties) | Yes ($variables) |
| Nesting | Yes (native) | Yes |
| Mixins | No | Yes |
| Loops/conditions | No | Yes |
| Colour functions | color-mix() | Full suite |
| File splitting | @import (needs bundler) | @use, @forward |
Tip: For new projects, start with plain CSS. Add Sass only if you need mixins, loops, or other features that native CSS does not support.
General best practices
- Use a consistent naming convention (BEM or similar)
- Keep selectors short and flat -- prefer
.card-titleoverdiv.card > h3.title - Avoid IDs for styling -- use classes exclusively
- Avoid
!importantunless there is no other way - Use custom properties for values that repeat (colours, spacing, fonts)
- Write mobile-first -- base styles for small screens,
min-widthmedia queries for larger - Delete unused CSS -- dead code grows silently and increases file size
- Use
box-sizing: border-boxglobally -- add the reset to every project - Comment section boundaries, not individual properties
- Run your styles through a linter (Stylelint) to catch errors and enforce conventions
What you learned
- BEM (Block__Element--Modifier) keeps naming predictable and specificity flat
- Organise CSS files from least specific (reset) to most specific (utilities)
- Use a CSS reset to remove inconsistent browser defaults
- Utility classes handle one-off adjustments; utility-first frameworks take this to the extreme
- Frameworks offer component-scoped styles (CSS Modules, Scoped styles) to solve global scope
- Sass is less necessary now that CSS has native variables and nesting
- Keep selectors flat, use classes, avoid
!important, and delete dead CSS
Next step
With good architecture in place, the final technical chapter covers debugging and common pitfalls -- how to use DevTools effectively and avoid the most frequent CSS mistakes.