CSS architecture, naming and design tokens

Naming conventions, component versus utility thinking, two tiers of tokens, file organisation and avoiding specificity wars.

Naming for predictability

A naming convention is not about aesthetics. It exists so that anyone can guess a class name correctly, and so that changing one element cannot accidentally change another.

/* block, element, modifier */
.card { }
.card__title { }
.card--featured { }

/* state that JavaScript toggles */
.toolbar[data-state="open"] { }

/* single-purpose utility */
.u-stack-2 > * + * { margin-block-start: var(--space-2); }
  • One class per element is the goal: flat specificity means any rule can be overridden by a later rule.
  • Describe structure, not appearance — .card--featured survives a redesign, .blue-box does not.
  • Use data attributes for state so CSS and JavaScript agree on one vocabulary.
  • Utilities are for repeatable single-property adjustments; components are for anything with structure.
  • Avoid element-qualified selectors such as .card div — they couple the stylesheet to markup you will change.

Two tiers of tokens

Design tokens are custom properties with names that describe purpose. Keeping a primitive tier and a semantic tier separate is what lets a theme change colours without touching a single component.

:root {
  /* primitives: the palette, never used directly by components */
  --blue-600: oklch(55% 0.18 258);
  --grey-050: #f6f7f9;
  --space-2: 0.5rem;

  /* semantics: the contract components depend on */
  --color-action: var(--blue-600);
  --color-surface: var(--grey-050);
  --radius-md: 6px;
}

[data-theme="dark"] {
  --color-surface: #17181c;
  --color-action: oklch(70% 0.14 258);
}
TierExampleChanges with a theme?
Primitive--blue-600No — the palette is fixed
Semantic--color-actionYes, and that is the point
Component--card-paddingRarely, and only locally
Scale--space-2No — but it can be adjusted globally
💡
A good token name is a promise: it says what the value is for, not what it looks like. --color-danger survives a rebrand; --red-500 has to be renamed the day the danger colour becomes orange.

File organisation and specificity discipline

Once layers settle the order, the rest of the architecture is about where a rule lives and how easily it can be deleted.

/* styles/main.css */
@layer reset, base, layout, components, utilities;

@import url("./reset.css") layer(reset);
@import url("./base.css") layer(base);
@import url("./layout.css") layer(layout);
@import url("./components/button.css") layer(components);
  • Colocate component CSS with the component when the tooling allows, so deleting a component deletes its styles.
  • Keep a fixed, documented z-index scale and a fixed breakpoint scale; ad-hoc numbers are how layouts become unpredictable.
  • Prefer deleting an override to stacking another one on top — the second override is where the arms race starts.
  • Never use an id selector for styling, and treat each !important as a defect to be removed.
  • Review with one question: can a new developer find the rule that styles this element in under a minute?

FAQ

How many tokens are too many?
When nobody can remember which one to reach for. Two tiers with a handful of semantic names — surface, text, border, action, danger — cover most interfaces. Add a token when a value is genuinely reused, not in advance.
BEM or utility classes?
They are complementary. Utilities are excellent for spacing and layout inside a component; BEM-style classes are clearer for a component's own structure and variants. Mixing them deliberately beats committing to either extreme.

Modern selectors and cascade control Color, backgrounds and dark mode

Last refreshed 2026-09-18.