01 — Foundation

Why CSS architecture matters

Good CSS stays predictable. Bad CSS hides problems until someone else inherits the codebase.

CSS should make interfaces easier to use and easier to maintain — not create specificity wars, !important graveyards, or files nobody dares to touch. When styling needs detective work, the system is failing the team.

These standards describe how to structure CSS so teams can read, debug, extend, and delete styles with confidence — built for real delivery, not clever one-offs.

See code formatting and style for indentation, declarations, and media-query layout in examples.

02 — Principles

Core principles

Defaults that keep styling calm and maintainable.

  • clarity over cleverness
  • consistency over novelty
  • composition over duplication
  • defaults over unnecessary complexity
  • progressive enhancement first
  • accessibility as part of styling, not an afterthought
  • performance matters — unused CSS still has a cost

03 — Architecture

Layered structure

Organise CSS by responsibility, not by whoever shouted loudest in the last sprint.

This site uses LSCSS (Layered Semantic CSS). Declare layer order once in your stylesheet entry file, then place styles in the matching layer — the order below is what this site follows.

Layer order

@layer legacy, settings, base, utilities, layout, components, theme, hacks;

Recommended folders

  • legacy/ — vendor CSS, old frameworks, third-party stylesheets (optional; sometimes called thirdparty elsewhere in LSCSS)
  • settings/ — non-colour tokens (spacing, type, radius, motion)
  • base/ — reset and global element defaults
  • utilities/ — skip link, .vh, .is_hidden, focus, motion helpers
  • layout/ — page shell, containers, regions
  • components/ — named UI blocks
  • theme/ — colour tokens and presentation overrides on :root
  • hacks/ — temporary fixes (should feel embarrassing)

Settings

Font faces, breakpoints, z-index scales, spacing scales. Non-colour configuration only — no rendered component styles.

Base

Typography, links, forms, tables, lists, media defaults. Low specificity. No component styling.

Utilities

Helpers for real repeated problems — not a second styling language that grows without discipline. Use .vh for visually hidden text that screen readers should still read — error prefixes, step status, tooltip descriptions linked with aria-describedby. Use .is_hidden only when content is removed from the page (display: none) — panels closed by JavaScript, not for screen-reader-only copy.

Components

Self-contained UI blocks with two-word class names where possible — for example .content-card, .site-header, .action-button. Styles should not depend heavily on page context.

Layout

Page shells, containers, and structural regions. Layout rules should not leak heavily into components.

Theme

Colour custom properties and theme-specific presentation — for examplecolors.css on :root. Keeps palette changes separate from component structure.

Hacks

Short-term fixes only. If hacks become permanent, refactor properly and move the code where it belongs.

Legacy

Quarantine CSS you do not own yet — Bootstrap remnants, widget vendor sheets, tag-manager injectors wrapped as imports. Import with layer(legacy) so settings, base, and components can override deliberately while you migrate. Empty on greenfield projects; essential on incremental rewrites.

04 — Cascade

Layers and specificity

Layer order beats source order. Specificity should stay low and predictable.

Use cascade layers intentionally. Declare order once, then place files in the right layer — fewer surprises than fighting selector weight.

Match the order declared in your entry file — on this site:legacy → settings → base → utilities → layout → components → theme → hacks. Unlayered CSS always wins over layered CSS, so a stray rule outside @layer can override your whole stack. Avoid unlayered styles: put every declaration in a layer and fix precedence by moving files or adjusting layer order — not by cranking specificity or parking rules outside layers.

Keep specificity low

Predictable selectors are easier to maintain and override safely.

Avoid

  • unlayered CSS — use layer order instead of winning the cascade by accident
  • IDs in selectors
  • deep nesting
  • long chained class lists
  • !important as architecture
  • over-qualified selectors (div.content-card ul li a)

Prefer

.content-card { }

.content-card > .title { }

.action-button--primary { }

.site-header.is_open { }

.site-header.is_open .primary-nav { }

Component blocks, children scoped by the parent, modifiers with--, generic state classes such as is_active andis_hidden — not BEM-style __element blocks or component-specific state names like is-nav-open.

05 — Naming

Names and state

Name intent, not visual accidents.

Component class names should use two words where possible (.site-header, .content-card) — not single words like .card or .hero. Names should explain purpose, not visual accidents or one-off layout hacks.

Good

/* Two-word components */
.site-header { }
.content-chapter { }
.content-card { }

/* Children scoped to the parent */
.site-header > .logo { }
.content-card > .title { }

/* Variants */
.action-button--primary { }
.content-chapter--tinted { }

/* Generic state */
.is_active { }
.is_hidden { }
.has_error { }

Bad

/* Single-word components — too generic */
.hero { }
.card { }
.button { }

/* Appearance, not role */
.red-text-box-left-small { }

State classes

Make state explicit with generic classes such as .is_active, .is_hidden, and .has_error — not names like.is-nav-open, and not mixed up with -- variants. Prefer class toggles over inline styles scattered from JavaScript unless there is a strong reason not to.

Do not use .is_hidden for screen-reader-only text — that removes content from the accessibility tree. Use the utility .vh instead. Reserve .is_hidden for UI you genuinely take off the page until opened or shown again.

06 — Tokens

Custom properties and units

Tokens create consistency. Units should respect how people actually use the web.

Use CSS custom properties for spacing, typography, colour, radii, shadows, motion, and z-index scales. Central tokens make global changes safer and faster.

Naming

Tokens you use dozens of times per file should stay short in the source — abbreviate the property, keep the role on the suffix. Document the map once in settings/tokens.css so the team does not guess.

Common abbreviations:

  • fw — font-weight (--fw-regular, --fw-bold)
  • fs — font-size (--fs-m, --fs-xl)
  • lh — line-height (--lh-tight)
  • br — border-radius (--br-s, --br-m)
  • ff — font-family (--ff-sans, --ff-mono)

Stepped scales share the same suffixes everywhere: xs, s, m, l, xl, 2xl (and3xl, 4xl when you need them) — not sm, md, or lg. Scales can drop the property prefix when the category is obvious — --space-m — as long as names stay consistent across the project. Do not mix long and short forms for the same value (--font-weight-bold and --fw-bold together).

Example

:root {
    --fw-regular: 400;
    --fw-bold: 700;
    --fs-m: 1.0625rem;
    --space-m: 1rem;
    --br-s: 0.25rem;
    --br-m: 0.625rem;
}

Units

Prefer rem, em, percentages, fr, clamp(), and logical sizing. Avoid layouts that break when users change zoom or font size.

Logical properties

Prefer logical axes (block and inline) over physical sides (top, right, bottom, left). They follow writing direction and reading mode, so layouts hold up for RTL, vertical scripts, and mixed locales without a separate override stylesheet.

The properties below are common examples — not an exhaustive list. Reach for the logical form of whatever you would otherwise write with physical sides or widths:

  • Margin and paddingmargin-block, margin-inline, margin-block-start, padding-inline-end, and the other *-block-* / *-inline-* longhands
  • Positioninginset, inset-block, inset-inline, inset-inline-start, inset-block-end
  • Sizeinline-size, block-size, min-inline-size, max-block-size
  • Bordersborder-inline-start, border-block-end-width, border-start-start-radius (corners follow the same axes)
  • Alignment and flowtext-align: start / end instead of left / right; float: inline-start where floats are still justified

Physical properties are fine when the design truly anchors to the viewport (for example a full-bleed hero tied to the top edge). Default to logical; document the exception when you deliberately choose physical sides.

07 — Layout

Layout and typography

Use the right tool for the axis you are solving.

Layout

Use Grid for two-dimensional layout. Use Flexbox for one-dimensional distribution. Pick based on the problem, not habit.

Typography

Typography is infrastructure. Prioritise readable line length, sensible line-height, clear hierarchy, and text sizes that work for real users — not decorative noise.

Line length

Cap prose width with max-inline-size (or max-width) using the ch unit — one ch is roughly the width of the 0 glyph in the current font, so 65ch targets about sixty-five characters per line regardless of font size. Store shared limits as tokens (for example --content-measure: 65ch for body copy, a shorter ch value for display lines).

.article-body {
    max-inline-size: 65ch;
}

.display-line {
    max-inline-size: 22ch;
}

ch is a hint, not a precise character count — mixed scripts, italics, and font metrics shift the real count. Tune by reading real content, not by chasing a perfect number in DevTools.

Text wrapping

text-wrap: balance evens out line lengths on short headings so you avoid a single orphan word on its own line. text-wrap: pretty does the same kind of tidy-up for body copy — fewer short last lines and lone words stranded at the end of a paragraph. Both are enhancements: copy stays readable without them.

.card-title {
    text-wrap: balance;
}

.card-body {
    text-wrap: balance;
    text-wrap: pretty;
}

For body copy, declare balance first, then pretty on the next line. Browsers that understand pretty use it; browsers that do not ignore the invalid value and keep balance — you do not need@supports for that fallback. Use @supports only when you want different rules entirely, not for this stack.

Browser support: ch is safe everywhere a modern baseline expects. text-wrap: balance is widely available in current evergreen browsers (Baseline 2024). text-wrap: pretty lags behind, but the cascade above is a safe progressive enhancement. Do not rely on either value for layout stability (no fixed heights that assume a line count).

Use balance on headings and short labels; reserve pretty for paragraphs and lists where orphan lines would show. Skip both on long continuous text if performance becomes measurable — the cost is usually negligible on typical pages, but worth knowing for very large DOMs.

08 — Motion

Motion and accessibility

Motion should clarify. Accessibility is part of styling.

Motion

Animation should support understanding, not compete for attention. Respect prefers-reduced-motion. Calm interfaces beat restless ones.

Accessibility

CSS constantly affects accessibility. Pay attention to:

  • visible focus styles
  • readable spacing and touch targets
  • colour contrast
  • zoom and reflow behaviour
  • reduced motion preferences

These are not “extra” checks — they are part of shipping quality frontend.

09 — Review

Before you approve

A short checklist for CSS in code review.

  • specificity stays low and predictable
  • naming describes intent, not one-off visuals
  • spacing and tokens are consistent
  • custom properties used where shared values matter
  • motion respects reduced-motion preferences
  • focus, contrast, and touch targets are supported
  • layout choice (grid vs flex) matches the problem
  • selectors are maintainable — no detective work required
  • CSS size is reasonable for what the page delivers
  • hacks are temporary, not permanent architecture
  • styles live in a declared layer — no unlayered rules

When in doubt, ask: Would another developer understand and safely change this CSS in six months? If not, simplify before you merge.