Chapter 17: CSS Architecture and Reusable Patterns
As projects grow, small style choices become structural decisions. This chapter introduces practical patterns for naming classes, shaping components, using CSS custom properties and organizing styles so that a site can expand without turning fragile. The goal is to create parts that are easy to find, easy to change and easy to reuse.
Class naming patterns
Clear class names make styles predictable. A name should hint at purpose, not just appearance, and it should follow a pattern that feels consistent from file to file. The following subsections show simple ways to keep names readable and scalable.
BEM and readable names
BEM stands for Block, Element and Modifier. A block is a standalone component; an element is a part of that component; a modifier expresses a variation. This structure reduces guesswork because the relationship is visible in the name itself.
.card { … }
.card__title { … }
.card__actions { … }
.card--featured { … }
Use full words for blocks and elements. Keep modifiers short and consistent, such as --large, --active and --muted.
Names that describe purpose instead of look
Names tied only to appearance tend to age quickly. Prefer intent such as .btn, .btn--primary and .notice instead of look only names such as .blue or .left. This leaves room for design changes without renaming classes across files.
/* Prefer intent */
.btn { … }
.btn--primary { … }
/* Avoid brittle names */
.blue { … }
.left { … }
Lightweight utility classes
Utilities are single purpose helpers such as spacing and layout nudges. They help compose designs without creating new components for tiny variations. Keep utilities short and consistent; document them in one place.
.u-m-0 { margin: 0; }
.u-px-2 { padding-left: .5rem; padding-right: .5rem; }
.u-flex { display: flex; }
.u-sr-only { position: absolute; width: 1px; height: 1px; clip: rect(0 0 0 0); overflow: hidden; }
State and behavior flags
Use clear flags for state that will change at runtime. Class flags such as .is-open, .is-active and .is-invalid make toggling styles easy for scripts and keep the style rule readable.
.accordion.is-open > .accordion__panel { display: block; }
.field.is-invalid input { border-color: var(--color-danger); }
Component level styling
A component should bundle structure, appearance and local variables in one place. This reduces side effects and makes reuse straightforward. The following subsections show practical patterns to keep components resilient.
Encapsulated blocks with predictable children
Define a parent class for each component, then target only its elements. This narrows the scope and limits accidental conflicts elsewhere on the page.
.card { border: 1px solid var(--surface-border); border-radius: .5rem; background: var(--surface); }
.card__title { margin: 0 0 .5rem 0; font-size: 1.125rem; }
.card__meta { font-size: .875rem; color: var(--text-muted); }
Composition with small utilities
Combine component rules with a few utilities when it keeps the stylesheet simpler. The component carries the meaning; the utility carries the tiny adjustment.
<article class="card u-m-0">
<h3 class="card__title">Title</h3>
<p class="card__meta">Meta</p>
</article>
@layer to control the cascade
The cascade can be made more predictable with layers. Place base rules first; then components; then utilities. Later layers win when selectors are equal in strength, which avoids overuse of !important.
@layer base, components, utilities;
@layer base {
:root { --text: #111; --surface: #fff; --surface-border: #e5e7eb; }
html, body { margin: 0; padding: 0; }
}
@layer components {
.card { … }
.btn { … }
}
@layer utilities {
.u-m-0 { margin: 0; }
.u-flex { display: flex; }
}
@layer with multiple files, ensure layer names match exactly in each file. A spelling mismatch creates a new layer and changes cascade order in ways that can surprise later.
Local tokens per component
Local tokens make components portable. Define private variables on the component root and reference them inside. This pattern keeps the interface small and clear.
.alert {
--alert-bg: var(--surface);
--alert-fg: var(--text);
--alert-border: var(--surface-border);
background: var(--alert-bg);
color: var(--alert-fg);
border: 1px solid var(--alert-border);
}
.alert--success { --alert-bg: #f0fdf4; --alert-border: #86efac; }
CSS variables and custom properties
CSS custom properties hold design tokens that the browser can inherit and update at runtime. They act a little like named storage in a programming language. A value is placed in a named slot and that name can be used wherever the value is needed. This helps keep designs consistent because the source of truth lives in one place. Custom properties support themes, user preferences and small context overrides without rewriting selectors.
Global tokens in :root
Place shared tokens in :root so that all components inherit the same base values. Keep names stable and group related values together.
:root {
--color-bg: #ffffff;
--color-fg: #111111;
--color-primary: #155eef;
--radius-regular: .5rem;
--space-2: .5rem; --space-3: .75rem; --space-4: 1rem;
}
Scoped overrides and theming
Wrap sections in a theme class and override only the tokens that change. This avoids duplication and keeps the component rules unchanged.
.theme-dark {
--color-bg: #0b1220;
--color-fg: #e5e7eb;
--color-primary: #93c5fd;
}
body { background: var(--color-bg); color: var(--color-fg); }
a { color: var(--color-primary); }
Fallbacks and computed values
Provide fallbacks with the second argument of var(). Use calc() with custom properties to derive consistent sizes from a single source.
.avatar {
width: var(--avatar-size, 40px);
height: var(--avatar-size, 40px);
border-radius: calc(var(--radius-regular) * 2);
}
User preference queries
Pair tokens with preference queries to respect motion and color choices without duplicating entire stylesheets.
@media (prefers-reduced-motion: reduce) {
:root { --motion-duration: 0ms; --motion-ease: linear; }
}
@media (prefers-color-scheme: dark) {
:root { --color-bg: #0b1220; --color-fg: #e5e7eb; }
}
Managing CSS as a site grows
Growth brings new pages, new contributors and new requirements. A few processes keep the stylesheet healthy: file structure, consistent layers, linting, naming rules and periodic pruning.
Layered file structure
Group styles by role, not by page. Keep base rules separate from components and utilities; this mirrors the cascade and keeps imports tidy.
/* index.css */
@layer base, components, utilities;
@import url("./css/base.css") layer(base);
@import url("./css/components/buttons.css") layer(components);
@import url("./css/components/cards.css") layer(components);
@import url("./css/utilities/spacing.css") layer(utilities);
Guardrails with linting and conventions
Automated checks catch drift. Use a linter to enforce patterns such as lowercase hex, no unused !important and consistent units. Add a short naming guide at the top of the repository so that new code follows the same rules.
/* Naming guide summary
Blocks: .card, .btn
Elements: .card__title
Modifiers: .btn--primary
States: .is-open, .is-invalid
Utilities: .u-*
*/
Prune unused rules
Old rules pile up over time. Periodically search templates for unused classes and remove them. A short checklist helps this become routine rather than a risky event.
/* Prune checklist
1. Search templates for class usage.
2. Remove rules with zero references.
3. Run visual tests.
4. Ship behind a feature flag when unsure.
*/
Document components where they live
Keep a small comment block above each component that explains purpose, public modifiers and any local tokens. This reduces ramp up time for new contributors.
/* Component: .badge
Purpose: small status label
Public modifiers: .badge--success, .badge--warning
Local tokens: --badge-bg, --badge-fg
*/
.badge { … }
.badge--success { … }
With clear names, layered components, shared tokens and a lightweight process for quality, your stylesheet remains calm as the project expands. The patterns in this chapter are small on purpose; they combine well and they are easy to teach to the next person who joins the work.