skip to content

Organizing Large Stylesheets

File and layer structure for a real codebase: reset, base, tokens, components, utilities — where global styles legitimately belong, and how you find and safely delete dead CSS. Usually the follow-up to any architecture question.

part ofCSSoverview, primer and where to startread it →
on this pageshow

questions

5

In a codebase where each UI component ships with its own stylesheet file, which kinds of CSS legitimately belong in the shared global stylesheet instead, and which do not?

level: middleimportance: must knowfreq 62%

answer

  1. would it die with the component?
  2. must exist once for the document
  3. inheritance is what makes base rules work
  4. deletion is the real constraint
  5. page-specific fixes are not global

basics

~20 s

Global CSS holds what must exist exactly once for the whole document: the reset, @font-face declarations, custom-property definitions on :root, base element typography, and a small utility set. Anything describing one component belongs in that component's file.

solid answer

~50 s

The useful test is: **if this component were deleted tomorrow, should this rule disappear with it?** If yes, the rule belongs in the component's own file. If no, it is global. That puts four things at document scope: the reset, `@font-face` declarations (a font family has to be registered once for the document), design-token custom properties declared on `:root` so every element in the tree can read them, and base element styling — `body` typography, default link colour, focus-visible styling — which works by inheriting down to everything. A short utility set can join them. What does *not* belong is anything selecting a component's internals, any page-specific fix, and any override written to defeat a component from outside. Those are the rules nobody dares delete two years later, because it is no longer obvious what they were propping up.

code

css · 33 lines
css
/* global.css - loaded once, before any component styles */

@font-face {
  font-family: "AppSans";
  src: url("/fonts/app-sans.woff2") format("woff2");
  font-display: swap;
}

:root {
  --color-text: #1a1a1a;
  --color-link: #0b5fff;
  --space-3: 1rem;
}

body {
  margin: 0;
  font-family: "AppSans", system-ui, sans-serif;
  line-height: 1.5;
  color: var(--color-text);
}

a {
  color: var(--color-link);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

go deeper

for a junior

Know that some CSS is document-wide — the reset, fonts, tokens, body typography — and that anything naming a specific component goes in that component's file. Give the reset and :root tokens as concrete examples.

for a middle

Explain the mechanism behind each global category: why @font-face registers once per document, why custom properties are declared on :root to be visible everywhere, and why base element rules reach everything through inheritance.

for a senior

Frame the split as a deletion strategy, not tidiness — CSS never errors on an unused rule, so matching a file's lifetime to what it styles is the only reliable way to keep the sheet shrinkable. Handle the grey zones: layout compositions and page-scoped fixes.

for a principal

Own the global stylesheet as a shared contract: define who reviews additions to it, why its size should stay roughly flat while component count grows, and how the entry file's order encodes the project's override rules for every team.

## The question that actually decides it Every architecture debate about global CSS collapses into one test: > If the component this rule was written for were deleted tomorrow, should this rule disappear too? "Yes" means the rule is component-scoped and belongs in the component's file, where deleting the component deletes the CSS as a side effect. "No" means it is genuinely document-level and belongs in the global stylesheet. The test is worth applying literally, because the failure mode of a global stylesheet is not that it is badly written — it is that it becomes undeletable. ## What genuinely belongs at document scope **The reset or normalize layer.** It exists to flatten browser defaults for the whole document. There is nothing per-component about it. **`@font-face` declarations.** An `@font-face` rule registers a family name for the document; it is not applied to any element. Declaring it once, globally, is the only sane arrangement — duplicating it in component files gives you multiple definitions of the same family name. **Design tokens as custom properties.** A custom property is inherited, and is only visible to the element it is declared on and that element's descendants. Declaring the palette and spacing scale on `:root` — the `<html>` element — makes them visible everywhere: ```css :root { --color-surface: #ffffff; --color-text: #1a1a1a; --space-3: 1rem; } ``` Theme variants that redefine those same properties (a `[data-theme="dark"]` block, a `prefers-color-scheme` media query) also belong here, because they are redefinitions of the same document-level contract. **Base element styling.** Body typography, the default link colour, `:focus-visible` styling, heading sizes. These are written against bare element selectors and reach everything through inheritance and through matching every instance of the element. A component file cannot express "all paragraphs in the app". **A small utility set, and print rules.** If the project has utilities (`.visually-hidden` is the archetype), they are shared by definition. `@media print` adjustments to the document as a whole live here too. ## What belongs in the component file Everything that names the component. Its layout, its states, its variants, its internal spacing, its media-query adjustments, and its `:hover`/`:focus`/`[aria-expanded]` rules. Media queries in particular are frequently misfiled: teams collect every breakpoint rule into a `responsive.css` at the end of the global sheet, which guarantees that deleting a component leaves its breakpoint rules behind. Keep the breakpoint next to the rule it modifies. ## The grey zone: layout and page-specific CSS Two categories fit neither bucket cleanly. **Layout compositions** — the app shell grid, the standard page container width, the stack/cluster primitives — describe *arrangement* rather than any single component. Give them their own layer of files (`layout/`), between base and components. They are shared, but they are not base styling, and mixing them into `base.css` is how a base file grows to a thousand lines. **Page-specific styling** — the one tweak the pricing page needs — should go in a file named after that page and loaded only for it, never appended to the global sheet. A page-scoped file is deletable when the page is; a rule appended to `global.css` with the comment `/* pricing page fix */` is not. ## Why the split is really about deletion CSS has no compile-time link between a rule and the markup it styles. Nothing errors when a selector matches nothing. That means the *only* reliable mechanism for removing dead CSS is structural: put the rule in a file whose lifetime matches the thing it styles. Colocation is not a tidiness preference — it is the deletion strategy. Every rule you move into the global sheet is a rule you have opted into keeping forever, because proving it unused later requires searching the entire codebase rather than deleting one folder. ## A workable order The global entry point ends up looking like a short, ordered list — reset, then base, then tokens (or tokens first, if base rules consume them), then layout, then utilities — with components composed in by the build. The order is itself a design artifact: it encodes which rules are allowed to override which, and a reviewer can read the whole architecture from that one file. The healthy signal is that the global stylesheet is roughly *stable in size*. Components come and go weekly; the reset, the tokens and the base typography should change only when the design system changes. A global file that grows every sprint is telling you that component-scoped rules are being filed at document scope.

  • Where do layout rules — the app shell grid, the standard page container width — belong under this split?
    In their own layer of files between base and components. They are shared, so they are not component-scoped, but they describe arrangement rather than document defaults, so folding them into `base.css` bloats the file that should be the most stable one in the project. A `layout/` folder keeps them findable and keeps base honest.
  • A designer asks for a one-off spacing tweak that applies only to the pricing page. Where does that CSS go?
    In a stylesheet named for that page and loaded only with it — not appended to the global sheet. The point is lifetime: when the pricing page is redesigned or removed, its CSS goes with it. A page fix filed globally becomes a rule no one can safely delete, because nothing records which page it was propping up.
  • Why can't a component's own stylesheet declare the design tokens it needs, so the component is fully self-contained?
    It can declare them, but they would only be visible on that component's subtree, so two components would hold independent copies of the same palette and could drift. Custom properties inherit downward from where they are declared, which is exactly why the shared vocabulary is declared once on `:root` and components only *consume* it with `var()`.
  • How would you tell, reviewing a pull request, that a rule has been filed in the global stylesheet when it shouldn't have been?
    Look at the selector. If it names a component (`.card__footer`), names a page, or is an override written to defeat a component from outside, it is misfiled — it will die with something and therefore belongs with that thing. Global rules should select bare elements, `:root`, or a genuinely shared utility class.

saying these in an interview costs you the question

  • Puts every media query in one global responsive.css file
  • Adds page-specific fixes to the bottom of global.css
  • Thinks global CSS is faster because it's a single file
  • Declares design tokens separately in each component file
  • Treats the base layer as a dumping ground for shared rules

context

open as a page

In CSS, what is the difference between a reset stylesheet and a normalize stylesheet, and where in a project's stylesheet order does it belong?

level: juniorimportance: should knowfreq 55%

basics

~20 s

A reset strips browser defaults — margins, list markers, heading sizes — down to a flat baseline. A normalize stylesheet keeps useful defaults and only patches cross-browser inconsistencies. Either one loads first, before base and component rules.

open as a page

In a CSS file the browser loads directly with a <link> tag, what rules govern where the @import at-rule may appear, and what does it cost at runtime compared with composing the same files at build time?

level: middleimportance: should knowfreq 45%

basics

~20 s

CSS @import rules must appear before any style rule — only @charset and @layer statements may precede them — and a misplaced one is silently ignored. At runtime each import is discovered only after its parent sheet is parsed, so requests serialize; a build step inlines them instead.

open as a page

You inherit a 12,000-line stylesheet and want to delete the rules nothing uses. How do you identify dead CSS, and why do coverage reports and automated purge tools over-report rules as unused?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Nothing in CSS errors on a selector that matches nothing, so deletion is evidence-gathering, not compilation. Coverage tools report only what a recorded session exercised, and purge tools match class-name strings in source, so states, breakpoints and dynamically built class names all look dead when they are not.

open as a page

You own frontend standards for an app whose global stylesheet grows every sprint and never shrinks. Which structural conventions and enforcement mechanisms actually stop that decay, and what does each one cost?

level: principalimportance: should knowfreq 30%

basics

~20 s

Stylesheets grow because adding a rule is always cheaper than understanding the existing ones, and nothing ever fails when a rule goes unused. The fixes are structural: colocate CSS with what it styles, keep the global layer a closed set with an owner, allow raw values only in the token file, and enforce it in CI rather than in review.

open as a page