Apply
Starter structure
Start with a predictable structure. The same layer folders and partials that keep ownership clear also support performance: a smallcritical.css for what every page needs immediately, andsite.css as the full import map for everything else, loaded when it will not delay first paint.
Real-world folder structure
css/
- critical.css# above-the-fold / every-page bundle
- site.css# full import map, loaded after critical
- settings/
- tokens.css
- media.css
- fonts.css
- base/
- reset.css
- typography.css
- helpers/
- vh.css
- a11y-link.css
- layout/
- site-head.css
- components/
- logo.css
- button.css
- main-nav.css
- card.css
- theme/
- brand.css
- legacy/
- vendor.css
- old-site.css
- hacks/
- temporary-fixes.css
Folder names may vary. Some teams group files undercomponents/ with subfolders. The important rule is ownership, not folder worship. The example puts vendor CSS underlegacy/ in one low bucket; on a greenfield project with no inherited first-party CSS you might use thirdparty/ instead (see legacy and third-party layers).critical.css and site.css sit at the root ofcss/ — delivery boundaries, not extra layers.
Critical and site CSS
LSCSS does not require two root files, but many production sites split delivery for performance. critical.css holds the smallest set of styles the browser needs early: tokens, reset, typography defaults, global layout chrome, and components that appear on every page (header, nav, buttons in the shell). Load it with normal priority so first paint is not waiting on card grids, legacy vendor CSS, or theme overrides for pages the user has not reached yet.
/* critical.css — small bundle for first paint on every page */
@layer legacy, settings, base, utilities, layout, components, theme, hacks;
@import 'settings/variables' layer(settings);
@import 'settings/fonts' layer(settings);
@import 'base/reset' layer(base);
@import 'base/typography' layer(base);
@import 'layout/site-head' layer(layout);
@import 'components/logo' layer(components);
@import 'components/button' layer(components);
@import 'components/main-nav' layer(components);site.css remains the full import map for the rest of the system — same @layer declaration, same partials, same ownership rules. It is not a dumping ground; it is the deferred bundle. Declare layers in both files so cascade order stays consistent when the second file arrives.
/* site.css — full import map; load after critical */
@layer legacy, settings, base, utilities, layout, components, theme, hacks;
@import 'legacy/vendor' layer(legacy);
@import 'legacy/old-site' layer(legacy);
@import 'settings/media' layer(settings);
@import 'helpers/vh' layer(utilities);
@import 'components/card' layer(components);
@import 'theme/brand' layer(theme);
@import 'hacks/temporary-fixes' layer(hacks);How you load the second file depends on your stack. The pattern below avoids blocking render on the full bundle while keeping anoscript fallback.
<!-- HTML: critical first, remainder without blocking first paint -->
<link rel="stylesheet" href="/css/critical.css" />
<link rel="stylesheet" href="/css/site.css" media="print" onload="this.media='all'" />
<noscript><link rel="stylesheet" href="/css/site.css" /></noscript>Keep critical.css small and stable. If a partial lands in critical because it is convenient, the bundle grows on every page visit. When in doubt, default to site.css and promote to critical only after you can justify it with metrics (header visible without flash, layout shift, and similar).
Site CSS as an import map
Whether you ship one file or two, the deferred bundle should define layer order first, then import partials into the correct layer. It should not become the place where emergency CSS goes to retire. See architecture for the same pattern on a single site.css project.
Tokens first
Use variables for spacing, typography, colour, border radius (--br-*), shadows, and layout decisions. Random values spread inconsistency fast and turn design into avoidable inconsistency. See design tokens for the naming system this site uses (--c-*, --fs,--space, and the rest).
:root {
--ff: 'Funnel Sans', system-ui;
--ff-display: 'Funnel Display', system-ui;
--fs: 1rem;
--fs-l: 1.5rem;
--space: 1rem;
--space-m: 1.5rem;
--c-red: hsl(343 100% 45.5%);
--c-blue: hsl(222 53% 35%);
--br-m: 8px;
--shadow-m: 0 2px 8px rgba(0, 0, 0, 0.12);
}Change the token, not twenty unrelated selectors hunting for the correct shade of red.
Fonts belong in settings
Font loading belongs in the settings layer. Typography defaults belong in base. Components should not be quietly importing font decisions like suspicious luggage.
@font-face {
font-family: 'Funnel Display';
font-style: normal;
font-display: swap;
font-weight: 300 800;
src: url('/fonts/funnel-display-variable.woff2')
format('woff2-variations');
}Working rules
critical.cssstays small;site.cssholds the full import map.- One component, one partial.
- No IDs for styling.
- Do not use
!importantto win cascade fights — with layers it often resolves the opposite of what you expect. Fix layer order instead (see!important and layers). - Base styles stay in the base layer.
- Utilities stay rare and purposeful.
- Theme changes presentation, not structure.
- Legacy CSS stays low in the cascade.
- Hacks are temporary and visible.
- Large files trigger review, not pride.
What to avoid
- One undivided CSS bundle when first paint matters — or a
critical.cssthat slowly absorbs the whole project. - Component styles mixed into reset or base files.
- Random colours and spacing values spread across components.
- Third-party CSS imported without a dedicated low layer (
thirdpartyorlegacy). - Temporary fixes hidden inside normal component files.
- Utility chains replacing actual component architecture.
If the project becomes difficult to explain, it will become worse to maintain.