CSS Architecture and Performance
| Field | Value |
|---|---|
| Type | Skill Resource |
| Source | ~/.copilot/skills/design/references/css/architecture.md |
| Description | Not specified |
Source Content
CSS Architecture and Performance
Contents:
- The two layers
- Sample tree
- Cascade layers
- Every component folder has a .css file
- Splitting for performance
- Never duplicate — share via generic classes
- Performance habits
Two questions decide where any rule lives: is it system-wide (resets, tokens, base element styles, layout primitives, print) or does it belong to one component? System-wide rules go in a small global structure; component-owned rules sit in a .css file next to that component. There is no third place.
The two layers
Global CSS structure
A handful of focused files own everything that crosses component boundaries: the reset, the design tokens, base element defaults, layout primitives, shared utilities, and print. These rarely change and load once. Keep this set small — if a file is growing a component’s private styles, those styles are in the wrong layer.
Co-located component CSS
Each component owns a folder, and its styles live in a .css file beside its markup. A styling change is found by opening the component’s folder, not by hunting a monolithic stylesheet. Co-location keeps the blast radius of an edit small and makes a component genuinely portable — folder in, folder out.
Sample tree
src/├── styles/ # the small global layer│ ├── reset.css # normalize/reset, box-sizing│ ├── tokens.css # :root custom properties + theme overrides│ ├── base.css # bare element defaults (body, a, headings)│ ├── layout.css # .stack / .cluster / .grid layout primitives│ ├── utilities.css # shared single-purpose classes│ ├── print.css # the ONE print stylesheet (see ./print-and-legal.md)│ └── index.css # declares @layer order, then @imports the rest└── components/ └── Button/ ├── Button.tsx # markup + behavior └── Button.css # this component's styles ONLYindex.css is the single entry: it declares the layer order first (below), then imports each file. Nothing else decides cascade priority.
Cascade layers
Declare the layer order once, at the top of index.css, before any rules. After this, source order and selector specificity stop deciding who wins — layer order does. A later layer always beats an earlier one, so a component override no longer needs a more specific selector or !important.
/* index.css — the cascade contract -------------------------------------- *//* Order is priority: later layers win. Declare it ONCE, before any import. *//* Utilities sit last so a one-off utility can always trim a component. */@layer reset, tokens, base, layout, components, utilities;
@import "./reset.css" layer(reset);@import "./tokens.css" layer(tokens);@import "./base.css" layer(base);@import "./layout.css" layer(layout);@import "./utilities.css" layer(utilities);/* Component files opt into the components layer themselves: *//* @layer components { .button { ... } } */With the order fixed, utilities beats components beats base regardless of how the selectors read. That is the whole point: predictable overrides without a specificity arms race. See hard-rules.md on keeping specificity flat — layers remove the reason most people reach for deep selectors.
Every component folder has a .css file
Every component folder contains a .css file even when it is empty. The empty file is the obvious, pre-agreed home for that component’s styles — so the next person adds a rule there instead of leaking it into a global file or an inline style. It keeps co-location honest: the question is never “where do styles for this go?”, only “what goes in here?”.
../scripts/check_colocation.py enforces it and fails when a component folder has no .css sibling. Run it before committing:
python3 ~/.copilot/skills/design/scripts/check_colocation.py src/componentsAn empty component .css should still carry its file-header comment so the home is labeled, not blank — see comments-and-naming.md.
Splitting for performance
Split CSS by concern, not into one bundle. Focused files let the bundler tree-shake and code-split, keep the critical path small, and mean a change to one component re-ships only that chunk.
| Practice | Why it helps |
|---|---|
| One file per concern (reset, tokens, layout…) | The bundler can split and cache each independently |
| Co-located component CSS | Ships with the component’s code-split chunk; unused routes don’t pull it |
print.css loaded with media="print" | The browser deprioritizes it on screen render — off the critical path |
| Defer non-critical CSS | Above-the-fold paint isn’t blocked by below-the-fold styles |
| Keep each file focused | Smaller files diff cleanly and dead rules are easier to spot and drop |
<!-- Print never blocks first paint: the browser fetches it at low priority. --><link rel="stylesheet" href="/print.css" media="print" />
<!-- Defer a non-critical sheet so it doesn't block above-the-fold paint. --><link rel="stylesheet" href="/below-the-fold.css" media="print" onload="this.media='all'"/>Never duplicate — share via generic classes
If the same declarations appear in two components, that pattern is not component-owned — it is shared. Extract it into one generic, reusable class (a utility or a shared component class) and reference it from both places. Copy-paste CSS is the main source of drift; one definition is one place to fix.
/* utilities.css — shared, reusable, used in many places ------------------ */
/* .stack: vertical rhythm owned by the LAYOUT, not the children. *//* Children stay margin-free; the parent owns the gap (see ./spacing.md). */.stack { display: flex; flex-direction: column; gap: var(--space-4);}
/* .card-surface: the shared "raised panel" look. Define ONCE, reuse. *//* Why a class, not a copy: themes flip the tokens in one spot. */.card-surface { background: var(--color-bg); border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: var(--space-4);}<!-- Both compose the shared class instead of re-declaring the look. --><article class="card-surface stack">…</article><aside class="card-surface stack">…</aside>Whether a repeated pattern becomes a utility class or a named component class is a BEM-vs-utility judgment — bem-and-tailwind.md has the decision table. The rule here is narrower: define it in exactly one place.
Performance habits
The browser pays for every selector on every recalculation. Keep that cost low by default.
- Prefer the cheapest selector. A single class (
.button) is the baseline. Match by class, not by tag-plus-descendant gymnastics. - Avoid deep descendant chains.
.card .body .row .labelis slow and brittle; flat.card__labelis faster and clearer. Flat selectors also keep specificity flat (see hard-rules.md). - Use
content-visibility/containfor big offscreen sections.content-visibility: autoskips layout and paint for off-screen blocks until they scroll near;contain: layout paintwalls off a subtree so its changes don’t trigger a whole-page reflow. - Never
transition: all. It animates properties you didn’t mean to and forces the engine to watch everything. Name the exact properties.
/* content-visibility: skip render work for an off-screen section. *//* contain-intrinsic-size reserves space so the scrollbar doesn't jump. */.feed-section { content-visibility: auto; contain-intrinsic-size: auto 600px;}
/* Transition the named properties only — never `all`. *//* Why: `all` watches every property and animates ones you never intended. */.button { transition: background-color var(--motion-fast), color var(--motion-fast);}