CSS Architecture & Modern Techniques
Writing CSS that works is easy. Writing CSS that scales — CSS that a team can maintain, extend, and debug six months later — is a different skill entirely. This lesson teaches you how to think about CSS structurally, name things consistently, and use modern native features that make your stylesheets easier to manage without any build tools or preprocessors.
- What CSS architecture is and why it matters
- How to use BEM naming conventions
- How to organize CSS files logically (pure CSS)
- How specificity and the cascade really work
- CSS custom properties, utility classes, and component-based thinking
- Modern features:
@layerand logical properties
1. What is CSS Architecture?
CSS architecture is the practice of organizing, naming, and structuring your CSS so that it stays predictable and maintainable as your project grows. It is not a single tool or framework — it is a set of decisions about how you write your styles.
Think of it like building a house. You could pile bricks randomly and eventually have four walls, or you could follow a plan that makes the house safe, extensible, and readable by any contractor who comes after you.
Problems with unstructured CSS
When CSS has no architecture these problems appear quickly:
- Specificity wars — selectors keep getting more specific to override each other
- Duplication — the same color value is written 40 times across 10 files
- Fragile overrides — changing one rule breaks three unrelated pages
- No clear ownership — nobody knows where a style lives or who wrote it
- Dead code — styles for components that no longer exist pile up
- Consistent naming makes selectors predictable and low-specificity
- CSS variables centralize repeated values in one place
- Component isolation means changes stay local
- File structure tells everyone exactly where to look
- Unused styles are easy to spot and delete
2. Naming Conventions — BEM
BEM stands for Block, Element, Modifier. It is a naming system that makes CSS class names self-documenting — you can read a class name and immediately understand what it styles and how it relates to the rest of the UI.
Block
A standalone UI component. The top-level "thing". Can exist on its own.
Element
A part inside a block. Uses double underscore. Cannot exist outside its block.
Modifier
A variation or state of a block or element. Uses double hyphen.
BEM in practice
/* ── Block ── */
.card {
background: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
/* ── Elements (parts inside the block) ── */
.card__image {
width: 100%;
border-radius: 6px 6px 0 0;
}
.card__title {
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.card__body {
color: #5a6c7d;
line-height: 1.6;
}
.card__footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1rem;
}
/* ── Modifiers (variants of the block) ── */
.card--featured {
border: 2px solid #3498db;
}
.card--compact {
padding: 0.75rem;
}
/* ── Element modifier ── */
.card__title--large {
font-size: 1.75rem;
}
And the matching HTML:
<!-- Default card -->
<article class="card">
<img class="card__image" src="photo.jpg" alt="Product">
<h2 class="card__title">Product Name</h2>
<p class="card__body">Short description here.</p>
<div class="card__footer">
<span>$29</span>
<button class="btn btn--primary">Buy</button>
</div>
</article>
<!-- Featured variant — just add the modifier class -->
<article class="card card--featured">
<h2 class="card__title card__title--large">Special Offer</h2>
<p class="card__body">...</p>
<div class="card__footer">...</div>
</article>
- Block names should describe what the component is:
.navbar,.modal,.avatar - Elements describe their role inside the block:
.navbar__link,.modal__close - Modifiers describe a state or variant:
--active,--disabled,--large - Never go more than two levels deep:
.card__body__textis a sign you need a new block
- Nesting BEM in CSS — BEM class names are already flat. Never write
.card .card__title; just use.card__titlealone. - Three-level elements —
.card__body__paragraphis wrong. Create a new block or re-think the structure. - Using BEM for layout — Grid containers and page-level wrappers don't need BEM. BEM is for reusable components.
3. File & Style Organization
Even without SCSS or a build tool you can split your CSS across multiple files linked in order
from <head>. Each file has a single responsibility — the same principle
good code always follows.
Link them in your HTML in this exact order — it matters for the cascade:
<head>
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/components.css">
<link rel="stylesheet" href="css/utilities.css">
</head>
What goes in each file
base.css — the foundation
/* base.css */
:root {
--color-primary: #3498db;
--color-accent: #2ecc71;
--color-text: #2c3e50;
--color-bg: #ffffff;
--space-xs: 0.5rem;
--space-sm: 1rem;
--space-md: 1.5rem;
--space-lg: 2rem;
--space-xl: 3rem;
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 16px;
--font-body: system-ui, sans-serif;
--font-code: 'Consolas', monospace;
}
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html { font-size: 16px; scroll-behavior: smooth; }
body {
font-family: var(--font-body);
color: var(--color-text);
background-color: var(--color-bg);
line-height: 1.6;
}
layout.css — page structure
/* layout.css */
.container {
width: 100%;
max-width: 1200px;
margin-inline: auto;
padding-inline: var(--space-md);
}
.page-grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-lg);
}
@media (min-width: 768px) {
.page-grid { grid-template-columns: 1fr 3fr; }
}
components.css — reusable UI
/* components.css */
.btn {
display: inline-flex;
align-items: center;
padding: 0.6em 1.4em;
border: 2px solid transparent;
border-radius: var(--radius-sm);
font-size: 1rem;
font-weight: 600;
cursor: pointer;
text-decoration: none;
transition: background-color 0.2s, transform 0.15s;
}
.btn:hover { transform: translateY(-1px); }
.btn--primary {
background-color: var(--color-primary);
color: #fff;
}
.btn--primary:hover { background-color: #2980b9; }
.btn--outline {
background-color: transparent;
border-color: var(--color-primary);
color: var(--color-primary);
}
utilities.css — one-job helper classes
/* utilities.css */
.mt-1 { margin-top: var(--space-xs); }
.mt-2 { margin-top: var(--space-sm); }
.mt-3 { margin-top: var(--space-md); }
.text-center { text-align: center; }
.text-right { text-align: right; }
.font-bold { font-weight: 700; }
.hidden { display: none; }
.sr-only { /* visible to screen readers only */
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
}
4. CSS Specificity & the Cascade
When two rules target the same element, the browser uses specificity to decide
which wins. Understanding specificity stops you from reaching for !important
every time something doesn't apply.
Specificity scoring
Every selector has a three-number score: (IDs, Classes, Elements). Higher score wins.
The cascade order
When specificity is equal, the rule that appears later in the file wins.
Strategies for managing specificity
- Stay flat. Use single class selectors (
.card) not chains (section div .card). - Avoid ID selectors in CSS. IDs have a specificity of (1,0,0) which is very hard to override without escalating.
- Never use
!importantin component or layout CSS. It is occasionally acceptable in utilities (e.g..hidden { display: none !important; }) where you genuinely want it to always win. - Use
:where()to write zero-specificity selectors in resets::where(h1, h2, h3) { margin: 0; }
/* ✗ Bad — high specificity chains */
#page-header nav ul li a { color: blue; }
/* ✓ Good — single class, easy to override */
.nav__link { color: blue; }
/* ✓ Zero-specificity reset using :where() */
:where(h1, h2, h3, h4) {
margin: 0;
line-height: 1.3;
}
5. CSS Custom Properties (Variables)
CSS custom properties let you define values once and reuse them everywhere. They are live — changing a variable instantly updates every place it is used. They cascade and inherit like regular CSS properties, and they work natively in every modern browser.
Defining and using variables
/* Define on :root to make globally available */
:root {
/* Colors */
--color-primary: #3498db;
--color-primary-dk: #2471a3;
--color-success: #2ecc71;
--color-danger: #e74c3c;
--color-text: #2c3e50;
--color-bg: #ffffff;
/* Spacing scale */
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 1rem; /* 16px */
--space-4: 1.5rem; /* 24px */
--space-5: 2rem; /* 32px */
--space-6: 3rem; /* 48px */
/* Typography */
--font-size-sm: 0.875rem;
--font-size-md: 1rem;
--font-size-lg: 1.25rem;
--font-size-xl: 1.5rem;
/* Border radii */
--radius-sm: 4px;
--radius-md: 8px;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 3px rgba(0,0,0,0.12);
--shadow-md: 0 4px 12px rgba(0,0,0,0.15);
}
/* Use them with var() */
.btn--primary {
background-color: var(--color-primary);
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-sm);
}
.btn--primary:hover {
background-color: var(--color-primary-dk);
}
Dark mode with one variable swap
The real power of CSS variables shows when implementing themes. You redefine the variables, and every component updates automatically:
:root {
--color-bg: #ffffff;
--color-text: #2c3e50;
--color-surface: #f8f9fa;
--color-border: #e1e4e8;
}
/* Dark mode — swap the values, not the rules */
[data-theme="dark"] {
--color-bg: #1a1a1a;
--color-text: #e8e8e8;
--color-surface: #2d2d2d;
--color-border: #404040;
}
/* Components use variables — automatically themed */
body {
background-color: var(--color-bg);
color: var(--color-text);
}
.card {
background: var(--color-surface);
border: 1px solid var(--color-border);
}
You can provide a fallback in case the variable is not set:
color: var(--color-text, #333);
- Defining variables inside a component selector instead of
:root— they won't be available globally. - Naming variables by their value (
--blue) instead of purpose (--color-primary). When you change the primary color to green,--bluebecomes confusing.
6. Utility Classes
A utility class does exactly one thing. Instead of writing custom CSS for every small tweak, you compose existing utility classes directly in HTML. This approach is popularized by Tailwind CSS, but you can implement a useful subset in plain CSS.
/* utilities.css */
/* Spacing */
.mt-0 { margin-top: 0; }
.mt-1 { margin-top: var(--space-2); } /* 8px */
.mt-2 { margin-top: var(--space-3); } /* 16px */
.mt-3 { margin-top: var(--space-4); } /* 24px */
.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: var(--space-2); }
.mb-2 { margin-bottom: var(--space-3); }
.p-1 { padding: var(--space-2); }
.p-2 { padding: var(--space-3); }
.p-3 { padding: var(--space-4); }
/* Text */
.text-center { text-align: center; }
.text-right { text-align: right; }
.text-sm { font-size: var(--font-size-sm); }
.text-lg { font-size: var(--font-size-lg); }
.font-bold { font-weight: 700; }
/* Display */
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.gap-1 { gap: var(--space-2); }
.gap-2 { gap: var(--space-3); }
/* Colors */
.text-primary { color: var(--color-primary); }
.text-muted { color: var(--text-secondary, #6c757d); }
/* Visibility */
.hidden { display: none !important; }
.invisible { visibility: hidden; }
Using utilities alongside component classes:
<!-- The card component provides structure,
utilities handle spacing tweaks without touching the CSS -->
<article class="card mt-3">
<h2 class="card__title text-lg font-bold">Hello</h2>
<p class="card__body text-muted">Some text.</p>
<div class="card__footer flex justify-between items-center">
<span class="text-sm">Posted today</span>
<a href="#" class="btn btn--primary">Read more</a>
</div>
</article>
Pros
- You rarely need to write new CSS for layout tweaks
- HTML is self-documenting — styles are visible right there
- Small, predictable changes with no side effects
- CSS file size stabilises as the project grows
Cons
- HTML can get verbose with many classes
- Duplication moves to HTML instead of CSS
- Not a substitute for proper component structure
- Can be misused to avoid thinking about design
Use component classes for the core structure and appearance of a UI element. Use utilities for spacing, typography, and alignment tweaks that vary from context to context. Never build a whole component out of utilities alone — that leads to the duplication problem in HTML.
7. Component-Based Thinking
A component is a self-contained, reusable piece of UI. Component-based CSS means each component owns its own styles and does not depend on where it appears on the page.
Rules for component CSS
- A component should look the same whether it appears in a sidebar or a modal
- Style with classes, never with tag selectors inside a component
- Do not reference page-level IDs or ancestor selectors inside a component
- Responsive behaviour belongs inside the component, not outside it
/* ✗ Bad — styles depend on page location */
.sidebar .card { padding: 0.5rem; }
#dashboard .card h2 { font-size: 1rem; }
/* ✓ Good — component is self-contained */
.card { padding: var(--space-4); }
.card--compact { padding: var(--space-2); } /* modifier for tight contexts */
.card__title { font-size: var(--font-size-lg); }
Thinking in components — a checklist
- Can I drop this HTML + CSS into a different page and have it work? (It should.)
- Is the component styling free of parent selectors?
- Do variants use modifier classes rather than duplicate rules?
- Are all magic numbers replaced with CSS variables?
8. Layout & Structure Best Practices
Avoid deeply nested selectors
/* ✗ Bad — brittle and hard to maintain */
body main section.articles div.wrapper article.post h2.title a {
color: blue;
}
/* ✓ Good — one predictable class */
.post__title-link { color: var(--color-primary); }
Prefer class-based styling
Styling with element type selectors causes unintended side effects. Use classes for everything that needs custom styles.
/* ✗ Risky — affects every h2 on the page */
h2 { font-size: 2rem; color: navy; }
/* ✓ Safe — only affects cards */
.card__title { font-size: 1.25rem; color: var(--color-text); }
/* ✓ Acceptable — in base.css as a reset */
h1, h2, h3 { line-height: 1.3; margin-bottom: 0.5em; }
Keep styles predictable
- One class should always produce the same visual result, regardless of context
- Avoid relying on sibling or child combinators (
+,~,>) outside of base resets - Group related rules together — don't scatter a component's styles across the file
9. Responsive & Scalable CSS
CSS that hardcodes pixel values breaks on different screens and at different font sizes. Writing flexible CSS from the start avoids the painful retrofitting of responsive styles later.
Use flexible units
/* ✗ Hardcoded — breaks at different font sizes and screens */
.card {
width: 320px;
padding: 24px;
font-size: 16px;
}
/* ✓ Flexible — scales naturally */
.card {
width: 100%; /* fills its container */
max-width: 40rem; /* caps at reasonable size */
padding: var(--space-4);
font-size: var(--font-size-md);
}
| Unit | Relative to | Best used for |
|---|---|---|
rem | Root font size (16px default) | Spacing, font sizes, component sizing |
em | Parent element font size | Padding/margin that should scale with the component's own text |
% | Parent container size | Widths and heights relative to parent |
vw / vh | Viewport width/height | Full-screen sections, hero images, viewport-relative type |
ch | Width of "0" character | Readable line lengths (max-width: 65ch) |
clamp() | Viewport + min/max bounds | Fluid typography and spacing |
Fluid typography with clamp()
/* Font grows from 1rem at narrow screens to 1.5rem at wide */
h1 {
font-size: clamp(1.5rem, 4vw, 2.5rem);
}
/* Padding grows with the viewport */
.section {
padding-block: clamp(var(--space-4), 6vw, var(--space-6));
}
Mobile-first media queries
/* Start with mobile — add complexity upward */
.card-grid {
display: grid;
grid-template-columns: 1fr; /* 1 column on mobile */
gap: var(--space-3);
}
@media (min-width: 600px) {
.card-grid { grid-template-columns: 1fr 1fr; } /* 2 columns */
}
@media (min-width: 960px) {
.card-grid { grid-template-columns: repeat(3, 1fr); } /* 3 columns */
}
10. Optional Modern Features
CSS @layer — explicit cascade control
@layer lets you declare a cascade layer order. Styles in a later layer win,
regardless of specificity within that layer. This makes specificity battles almost disappear.
/* Declare layer order first */
@layer base, components, utilities;
@layer base {
/* low-priority foundational styles */
body { font-family: system-ui, sans-serif; }
a { color: inherit; }
}
@layer components {
/* component styles — override base */
.btn { padding: 0.5em 1em; background: var(--color-primary); }
}
@layer utilities {
/* utilities always win over components */
.hidden { display: none; }
.text-center { text-align: center; }
}
@layer is supported in all modern browsers (Chrome 99+, Firefox 97+, Safari 15.4+).
It is safe to use in new projects today.
Logical properties — writing direction agnostic CSS
Physical properties like margin-left and padding-top are tied to
screen direction. Logical properties adapt automatically for right-to-left languages and
vertical writing modes.
| Physical property | Logical equivalent | Meaning |
|---|---|---|
margin-left / right | margin-inline | Both inline (horizontal) sides |
margin-top / bottom | margin-block | Both block (vertical) sides |
padding-left | padding-inline-start | Start of inline axis |
padding-right | padding-inline-end | End of inline axis |
width | inline-size | Size along the inline axis |
height | block-size | Size along the block axis |
/* Old way — direction-specific */
.container {
margin-left: auto;
margin-right: auto;
padding-top: 2rem;
padding-bottom: 2rem;
padding-left: 1.5rem;
padding-right: 1.5rem;
}
/* Logical way — works for LTR and RTL */
.container {
margin-inline: auto;
padding-block: 2rem;
padding-inline: 1.5rem;
}
11. Bad vs Good: Side-by-Side
Here is the same card component written two ways. Both produce the same visual output, but only one is maintainable.
div div.card h2 {
font-size: 20px;
color: #2c3e50 !important;
font-weight: bold;
}
.card p {
color: #5a6c7d;
font-size: 14px;
}
div.card {
background: white;
padding: 24px;
border: 1px solid #e1e4e8;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.card a.btn {
background: #3498db;
color: white !important;
padding: 8px 16px;
text-decoration: none;
border-radius: 4px;
display: inline-block;
}
/* In components.css */
.card {
background: var(--color-bg);
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
}
.card__title {
font-size: var(--font-size-lg);
font-weight: 700;
color: var(--color-text);
}
.card__body {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
/* In components.css — shared button */
.btn--primary {
background: var(--color-primary);
color: #fff;
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-sm);
text-decoration: none;
display: inline-block;
}
Key differences in the structured version:
- No
!important— specificity is managed through flat class selectors - No hardcoded values — everything references a CSS variable
- No tag-based selectors inside components — only BEM class names
- The
.btn--primaryclass is reusable anywhere, not locked to cards
12. Refactoring Example
Let's walk through converting a poorly written navbar into structured CSS step by step.
/* ✗ Original — tangled, hardcoded, hard to maintain */
#main-nav {
background: #1a1a2e;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
height: 60px;
}
#main-nav .logo {
color: white;
font-size: 22px;
font-weight: 800;
text-decoration: none;
}
#main-nav ul {
list-style: none;
display: flex;
gap: 10px;
margin: 0;
padding: 0;
}
#main-nav ul li a {
color: rgba(255,255,255,0.75);
text-decoration: none;
padding: 6px 12px;
font-size: 15px;
}
#main-nav ul li a:hover {
color: white !important;
background: rgba(255,255,255,0.1);
border-radius: 4px;
}
We have: a navbar block, with a logo element, a links list element, and each link element. The hover state is a modifier/state.
/* base.css — add nav-specific variables */
:root {
--nav-bg: #1a1a2e;
--nav-height: 60px;
--nav-link-color: rgba(255,255,255,0.75);
--nav-link-hover-color: #ffffff;
--nav-link-hover-bg: rgba(255,255,255,0.1);
}
/* components.css — navbar component */
.navbar {
background-color: var(--nav-bg);
padding-inline: var(--space-4);
display: flex;
align-items: center;
justify-content: space-between;
block-size: var(--nav-height);
}
.navbar__logo {
color: #fff;
font-size: var(--font-size-lg);
font-weight: 800;
text-decoration: none;
}
.navbar__links {
list-style: none;
display: flex;
gap: var(--space-2);
}
.navbar__link {
color: var(--nav-link-color);
text-decoration: none;
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-sm);
transition: color 0.2s, background-color 0.2s;
}
.navbar__link:hover,
.navbar__link--active {
color: var(--nav-link-hover-color);
background-color: var(--nav-link-hover-bg);
}
<nav class="navbar">
<a href="/" class="navbar__logo">MySite</a>
<ul class="navbar__links">
<li><a href="/" class="navbar__link navbar__link--active">Home</a></li>
<li><a href="/about" class="navbar__link">About</a></li>
<li><a href="/contact" class="navbar__link">Contact</a></li>
</ul>
</nav>
What we gained:
- No ID selector — the navbar can now be used multiple times or in any context
- No
!important— hover works naturally via flat specificity - Active state is a modifier class (
--active) not an JS-injected inline style - All values come from variables — theming is one variable change away
- Logical properties (
padding-inline,block-size) used for better RTL support
13. Mini Challenge
Challenge: Style a Profile Card
Given the HTML below, write CSS following all the principles from this lesson:
BEM naming, CSS variables, flat selectors, and no !important.
<!-- Starter HTML — do not change this -->
<article class="profile-card">
<div class="profile-card__avatar">
<img src="avatar.jpg" alt="Jane Doe">
</div>
<div class="profile-card__info">
<h2 class="profile-card__name">Jane Doe</h2>
<p class="profile-card__role">Frontend Developer</p>
<p class="profile-card__bio">Building clean, accessible UIs one component at a time.</p>
</div>
<div class="profile-card__actions">
<a href="#" class="btn btn--primary">Follow</a>
<a href="#" class="btn btn--outline">Message</a>
</div>
</article>
:rootborder-radius!important, no ID selectors, no tag selectors in component rulesbox-shadow).profile-card--compact with reduced padding:root {
/* your variables here */
}
.profile-card { /* ... */ }
.profile-card:hover { /* ... */ }
.profile-card--compact { /* ... */ }
.profile-card__avatar { /* ... */ }
.profile-card__avatar img { /* ... */ }
.profile-card__name { /* ... */ }
.profile-card__role { /* ... */ }
.profile-card__bio { /* ... */ }
.profile-card__actions { /* ... */ }
/* Responsive */
@media (max-width: 480px) {
.profile-card { /* ... */ }
}
14. Quick Reference Summary
| Principle | Rule |
|---|---|
| Naming | Use BEM: .block__element--modifier |
| Specificity | Keep selectors flat — one class per rule whenever possible |
| !important | Avoid in components; acceptable only in utilities |
| IDs in CSS | Never use for styling — only for JS hooks |
| Values | All colors, spacing, radii → CSS variables in :root |
| File structure | base → layout → components → utilities (in link order) |
| Components | Self-contained; no parent selectors; variants via modifiers |
| Responsive | Mobile-first; use rem, %, clamp(); avoid px for sizes |
| Nesting | Never more than 2 levels deep in CSS |
| Modern | Use @layer and logical properties in new projects |
Good CSS architecture is not about following rules for their own sake. It is about making your code readable and changeable. If a teammate (or future you) can open your stylesheet, find the right rule in under 30 seconds, and change it without fear — you have succeeded.