CSS
Last updated:
Practical CSS standards for layered architecture, low specificity, tokens, and stylesheets teams can maintain.
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
thirdpartyelsewhere 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
!importantas 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 padding —
margin-block,margin-inline,margin-block-start,padding-inline-end, and the other*-block-*/*-inline-*longhands - Positioning —
inset,inset-block,inset-inline,inset-inline-start,inset-block-end - Size —
inline-size,block-size,min-inline-size,max-block-size - Borders —
border-inline-start,border-block-end-width,border-start-start-radius(corners follow the same axes) - Alignment and flow —
text-align: start/endinstead ofleft/right;float: inline-startwhere 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.