Codeskill

Learn to code, step by step

Complex component systems with cascade layers

Complex component systems built on cascade layers. The intermediate tutorial introduced @layer; here we scale that to real projects with third-party CSS, design systems, page-level overrides, and multiple authors who will not read your README.

The problem at scale

One stylesheet with four layers works fine. A product with a reset, a vendor UI kit, a shared design system, app-specific components, marketing pages, and A/B test overrides does not. Without an explicit layer contract, specificity wars return – just slower and more expensive.

Layers are not magic. They are a shared agreement about precedence. Document the stack once, enforce it in code review, and treat unlayered CSS as a smell unless it lives in a named catch-all layer at the end.

A production layer stack

A stack that survives hand-offs between teams might look like this:

@layer
  vendor-reset,
  vendor-components,
  tokens,
  base,
  layout,
  components,
  utilities,
  overrides;

@import url("normalize.css") layer(vendor-reset);
@import url("vendor-ui.css") layer(vendor-components);
@import url("tokens.css") layer(tokens);
@import url("base.css") layer(base);
@import url("layout.css") layer(layout);
@import url("components.css") layer(components);
@import url("utilities.css") layer(utilities);

Vendor CSS sits low. Your tokens and base styles sit above it. Components beat vendor widgets. Utilities beat components for deliberate one-offs. The overrides layer is for documented exceptions – not for ‘I needed to win today’ hacks.

Sub-layers inside components

Large component libraries can declare nested layers within a file or import:

@layer components {
  @layer primitives, patterns, compositions;

  @layer primitives {
    .btn { … }
    .input { … }
  }

  @layer patterns {
    .form-row { … }
    .media-object { … }
  }

  @layer compositions {
    .checkout-panel { … }
  }
}

Later sub-layers beat earlier ones inside the parent layer. Compositions can override primitives without selector arms races. Name sub-layers for what they contain, not for priority feelings.

Layer-aware component APIs

Publish components with low-specificity selectors inside the components layer. Consumers override via utilities, tokens, or the overrides layer – not by copying your selectors with extra classes prepended.

@layer components {
  :where(.ds-card) {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding: var(--space-4);
    background: var(--surface-raised);
    border-radius: var(--radius-md);
  }

  :where(.ds-card__title) {
    font-size: var(--text-lg);
    font-weight: 600;
  }
}

:where() keeps specificity at zero inside the layer. A single utility class or a token change on a parent can restyle the card without fighting .ds-card.ds-card--featured h3.

Integrating third-party CSS

When a library does not support layers, wrap its import in a low layer and never edit the vendor file directly:

@import url("bootstrap.min.css") layer(vendor-components);

@layer components {
  :where(.btn) {
    /* your button replaces vendor look without !important */
    border-radius: var(--radius-pill);
    font-weight: 600;
  }
}

If the vendor uses heavy !important, your options narrow: fork, shadow DOM, or accept a parallel stylesheet loaded with lower priority. Layers fix ordering between your code; they do not rewrite someone else’s important declarations.

Feature flags and experiments

A/B tests and feature flags often inject CSS. Give experiments their own layer above components but below overrides, so winning variants merge cleanly and losing ones delete one file:

@layer experiments;

@layer experiments {
  [data-exp="checkout-v2"] .summary {
    position: sticky;
    top: 1rem;
  }
}

Governance

  • Put the layer order in one file everyone imports first.
  • Lint for unlayered rules in CI where possible.
  • Code review question: ‘Which layer does this belong in?’
  • Document what may never go in overrides (brand colours, typography scale).

The next tutorial covers advanced container query patterns – size and style queries combined, fallbacks, and component libraries that respond to their slot without viewport hacks.

PreviousGoing deep with CSS