CSS Architecture & Best Practices

Small stylesheets are easy. Large ones, maintained by many people for years, need structure: consistent naming, a design system of tokens, predictable specificity, and tooling. This lesson covers how teams keep CSS manageable — and fast.

Start with a reset and base styles

CSS reset.css
*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

img,
picture,
video,
canvas,
svg {
  display: block;
  max-width: 100%;
}

input,
button,
textarea,
select {
  font: inherit;
}

p,
h1,
h2,
h3 {
  overflow-wrap: break-word;
}

A small modern reset removes the most common browser inconsistencies and annoyances. Base styles then set typography and colours for plain elements.

Design tokens

CSS tokens.css
:root {
  /* colour */
  --color-text: #10131c;
  --color-muted: #565d72;
  --color-accent: #d6336c;
  --color-surface: #ffffff;

  /* spacing scale */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 1rem;
  --space-4: 1.5rem;
  --space-5: 2.5rem;

  /* type scale */
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-lg: 1.25rem;
  --text-xl: clamp(1.75rem, 1.2rem + 2vw, 2.5rem);

  /* other */
  --radius: 8px;
  --shadow-1: 0 2px 6px rgb(0 0 0 / 10%);
}

Tokens are the design's decisions captured as variables. Using var(--space-3) instead of a random 17px keeps spacing consistent, and a redesign becomes a matter of changing tokens.

Naming methodologies

BEM

CSS bem.css
.card { padding: 1rem; }
.card__title { font-size: 1.25rem; }
.card__button { margin-top: 0.5rem; }
.card--featured { border: 2px solid gold; }

Block, Element, Modifier: every class says which component it belongs to. Flat, low-specificity selectors and no surprises about what a class affects.

Utility-first

HTML utilities.html
<div class="p-4 rounded-lg shadow bg-white">
  <h3 class="text-lg font-bold mb-2">Card title</h3>
</div>

Frameworks like Tailwind CSS provide thousands of single-purpose classes and you compose them in HTML. It avoids naming things and dead CSS, at the cost of longer class attributes. Many teams combine approaches: components for complex pieces, utilities for small adjustments.

Scoped CSS

Component frameworks often scope styles automatically — CSS Modules, Vue's <style scoped>, Svelte, and native @scope or Shadow DOM — so class names can't collide between components.

Organising files

TXT structure
css/
├── main.css          /* imports in layer order */
├── reset.css
├── tokens.css
├── base.css          /* element styles */
├── layout.css        /* page regions */
├── components/
│   ├── button.css
│   ├── card.css
│   └── nav.css
└── utilities.css
CSS main.css
@layer reset, tokens, base, layout, components, utilities;

@import url("reset.css") layer(reset);
@import url("tokens.css") layer(tokens);
@import url("base.css") layer(base);
@import url("components/button.css") layer(components);
@import url("utilities.css") layer(utilities);

Ordering files from general to specific, and putting them in cascade layers (lesson 45), keeps the cascade predictable. In production, a build tool bundles them into one file.

Specificity discipline

  • Style with classes; keep ids and long chains out of CSS.
  • Avoid !important except in utilities designed to always win.
  • Use :where() for low-specificity defaults.
  • Prefer adding a modifier class over writing a more specific selector.

Writing maintainable CSS

  • Name by purpose, not appearance: .alert-error, not .red-box.
  • Avoid magic numbers: top: 37px is a mystery; a token or a comment explains it.
  • Let content define size: prefer min-height, intrinsic sizing and gap over fixed dimensions and margins.
  • Delete unused CSS. Dead styles accumulate; tools can detect unused selectors.
  • Comment the why, especially for browser workarounds.

Performance

  • CSS blocks rendering: keep the critical stylesheet small, and don't @import chains at runtime without a bundler (each import is another round trip).
  • Animate transform and opacity; avoid animating layout properties.
  • Use content-visibility: auto for long, off-screen sections.
  • Limit web fonts and use font-display.
  • Minify and compress CSS in production.

Tooling

ToolPurpose
StylelintCatches errors (unknown properties, invalid values) and enforces conventions — every example in this course was checked with it
PrettierConsistent formatting
Lightning CSS / PostCSSBundling, minifying, adding fallbacks for older browsers
SassA preprocessor with mixins, functions and partials — less essential now that CSS has variables, nesting and layers
Browser DevToolsInspect computed styles, grid/flex overlays, coverage of unused CSS

Browser support

Before using a new feature, check its support on caniuse.com or MDN's compatibility tables. Thanks to CSS's forgiving error handling, you can often use new features as progressive enhancements: older browsers ignore what they don't understand. @supports lets you test for a feature explicitly:

CSS supports.css
.gallery {
  display: flex;
  flex-wrap: wrap;
}

@supports (display: grid) and (grid-template-rows: subgrid) {
  .gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  }
}