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

Folder structure

css/

  1. critical.css# above-the-fold / every-page bundle
  2. site.css# full import map, loaded after critical
  3. settings/
  4. tokens.css
  5. media.css
  6. fonts.css
  7. base/
  8. reset.css
  9. typography.css
  10. helpers/
  11. vh.css
  12. a11y-link.css
  13. layout/
  14. site-head.css
  15. components/
  16. logo.css
  17. button.css
  18. main-nav.css
  19. card.css
  20. theme/
  21. brand.css
  22. legacy/
  23. vendor.css
  24. old-site.css
  25. hacks/
  26. 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.

CSS code example
/* 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.

CSS code example
/* 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 code example
<!-- 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).

CSS code example
: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.

CSS code example
@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.css stays small; site.css holds the full import map.
  • One component, one partial.
  • No IDs for styling.
  • Do not use !important to 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 acritical.css that 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 (thirdparty or legacy).
  • 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.