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--featuredsurvives a redesign,.blue-boxdoes 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);
}| Tier | Example | Changes with a theme? |
|---|---|---|
| Primitive | --blue-600 | No — the palette is fixed |
| Semantic | --color-action | Yes, and that is the point |
| Component | --card-padding | Rarely, and only locally |
| Scale | --space-2 | No — 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
!importantas 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.
Related
Modern selectors and cascade control Color, backgrounds and dark mode
Last refreshed 2026-09-18.