GuidePorchlight CSS

Architecture & layer model

How Porchlight is layered, and how host applications override it.

51 components9 patterns46 stable5 experimental

Command

Search Porchlight

Architecture & the layer model

Porchlight ships full and partial CSS bundles authored as small modules. Every framework rule lives inside one top-level cascade layer: porchlight.

The wrap

@layer porchlight {
  @layer reset, tokens, themes, base, layout, components, utilities,
    enhancements, print;
}

Inside porchlight, nine sub-layers declare a deterministic order. For normal stylesheet declarations, later layers win: components beat base and utilities beat components. !important reverses layer precedence.

Layer Purpose
reset Minimal browser normalization only.
tokens Primitive and semantic design tokens.
themes Theme, brand, density, contrast, and mode overrides.
base HTML element defaults.
layout Layout primitives and page/app shells.
components Component classes (.pl-c-button, .pl-c-card, …).
utilities Small single-purpose helpers.
enhancements Progressive @supports-gated features.
print Print behavior.

How a host app overrides Porchlight

Place the framework in the cascade before you import it:

/* app.css */
@layer porchlight, app; /* app wins */
@import "@cawalch/porchlight";

@layer app {
  /* your product styles here */
}

Normal unlayered styles beat normal layered styles. Important declarations follow different precedence; Porchlight uses them for safeguards such as hidden content, reduced motion, and print visibility. See MDN cascade-layer precedence.

Do not pass layer(...) to the @import. Each Porchlight module self-wraps in @layer porchlight.<name>; adding layer(...) would double-nest it.

Why no internal overrides layer?

Some frameworks ship a trailing overrides layer for consumers. With the porchlight wrap that’s redundant: overrides live in your own layers outside porchlight (or unlayered). One wrap, one mental model.

Overriding component tokens

Each component defines --pl-c-* tokens inside a @scope block (e.g. @scope (.pl-c-card) { … }). The scope root does not add specificity to the rules inside it. Layer order takes precedence over selector specificity and scope proximity for normal stylesheet declarations.

This means you can override component tokens from plain selectors in @layer app — no specificity hacks needed:

@layer app {
  /* This wins over @scope (.pl-c-card) because layer order beats scope. */
  .pl-c-card {
    --pl-c-card-padding: var(--pl-space-6);
  }
}

How it works

  1. All Porchlight rules are in @layer porchlight.*.
  2. Your rules are in @layer app (declared after porchlight).
  3. The cascade resolves layers in declared order. Since app comes after porchlight, competing normal stylesheet declarations in app win — regardless of selector specificity or @scope proximity.
  4. You only need higher specificity if you’re overriding within the same layer (e.g., overriding one Porchlight component from another Porchlight component). That’s not a consumer concern.

If an override is not working, check the layer order, selector, and target element. Component-local tokens must be set on the component root: a value on an ancestor cannot override a default declared directly on the component.