---
title: 'Scoped Styling Approaches'
source: 'https://academia.sh/en/courses/layout-and-responsive-design/scoped-styling-approaches'
course: 'Layout Systems and Responsive Design'
language: en
updated: '2026-08-17T18:09:29+00:00'
license: 'CC BY-SA 4.0'
---

# Scoped Styling Approaches

Approaches that genuinely limit the scope of names; generated local names, class sharing by declaration hash, the document scope declaration, shadow tree encapsulation, and comparing the guarantees each approach gives.

A problem was left open at the end of the previous lesson. A naming methodology prevents
collisions, but only by **convention**: if two developers give the same block the same name,
no tool warns them, and the rules silently mix together.

This lesson takes up approaches that move a name's scope out of convention and into a
mechanism. First, what the word "scope" guarantees needs to be separated out.

## What Scope Guarantees

Style leakage is not a single event; it is three separate events.

**Name collision.** Two separate components use the same class name; one's rules also match
the other's elements.

**Leak-in.** A general rule written outside the component — `p`, `a`, `* + *`, and the like —
matches elements inside the component.

**Leak-out.** A rule written inside the component matches elements outside the component. This
is the risk every rule written with a descendant combinator carries.

An approach being counted as "scoped" does not mean it solves all three. The program below
first computes two generation methods, then compares which direction each of five approaches
blocks.

```js
// scoping.mjs — generating scoped local names, and sharing classes by declaration hash
import { createHash } from "node:crypto";

const hash = (text, length = 5) =>
  createHash("sha256").update(text).digest("hex").slice(0, length);

// --- 1. local name + file path -> unique generated name ---
const generate = (path, localName) => `${localName}_${hash(`${path}#${localName}`)}`;

const INPUTS = [
  ["components/measurement-card.css", "card"],
  ["components/measurement-card.css", "heading"],
  ["components/sidebar.css", "card"],          // same local name, different file
  ["components/sidebar.css", "heading"],
  ["components/measurement-card.css", "card"],         // same input: must generate the same name
];

console.log("--- mapping local names to generated names ---");
const generatedNames = new Map();
for (const [path, name] of INPUTS) {
  const g = generate(path, name);
  console.log(`  ${path.padEnd(34)} .${name.padEnd(10)} -> .${g}`);
  generatedNames.set(`${path}#${name}`, g);
}
const collision = new Set([...generatedNames.values()]).size !== generatedNames.size;
console.log(`  distinct input count: ${generatedNames.size}, generated name count: ${new Set([...generatedNames.values()]).size}, collision: ${collision}`);

// --- 2. sharing a class by the hash of the declaration set ---
const normalize = (declarations) =>
  declarations.map((d) => d.replace(/\s+/g, " ").trim()).sort().join(";");
const className = (declarations) => "b" + hash(normalize(declarations), 7);

const COMPONENTS = {
  "measurement-card": ["padding: 1.5rem", "border: 1px solid #ccd", "border-radius: 4px"],
  "sidebar-box":        ["border-radius: 4px", "padding:  1.5rem", "border: 1px solid #ccd"],  // same set
  "masthead":         ["padding: 1.5rem", "border: 1px solid #ccd"],
  "location-panel":   ["padding: 1.5rem", "border: 1px solid #ccd", "border-radius: 4px"],
};

console.log("\n--- class generated by the hash of the declaration set ---");
const generatedRules = new Map();
for (const [name, declarations] of Object.entries(COMPONENTS)) {
  const cls = className(declarations);
  generatedRules.set(cls, normalize(declarations));
  console.log(`  ${name.padEnd(18)} -> .${cls}`);
}
console.log(`  component count: ${Object.keys(COMPONENTS).length}, generated rule count: ${generatedRules.size}`);
for (const [cls, body] of generatedRules) console.log(`    .${cls} { ${body.replaceAll(";", "; ")} }`);

// --- 3. the guarantees scoping approaches give ---
console.log("\n--- which approach guarantees what ---");
const APPROACHES = [
  { name: "naming convention alone", nameCollision: false, leakIn: false, leakOut: false },
  { name: "generated local name",    nameCollision: true,  leakIn: false, leakOut: true },
  { name: "runtime generation",      nameCollision: true,  leakIn: false, leakOut: true },
  { name: "document scope declaration", nameCollision: false, leakIn: false, leakOut: true },
  { name: "shadow tree encapsulation", nameCollision: true,  leakIn: true,  leakOut: true },
];
console.log("approach".padEnd(26) + "name collision".padStart(16) + "leak-in".padStart(12) + "leak-out".padStart(12));
for (const a of APPROACHES) {
  const blocked = (v) => (v ? "blocked" : "not blocked");
  console.log(a.name.padEnd(26) + blocked(a.nameCollision).padStart(16) + blocked(a.leakIn).padStart(12) + blocked(a.leakOut).padStart(12));
}
```

```
--- mapping local names to generated names ---
  components/measurement-card.css    .card       -> .card_574af
  components/measurement-card.css    .heading    -> .heading_3232f
  components/sidebar.css             .card       -> .card_fb1b0
  components/sidebar.css             .heading    -> .heading_85231
  components/measurement-card.css    .card       -> .card_574af
  distinct input count: 4, generated name count: 4, collision: false

--- class generated by the hash of the declaration set ---
  measurement-card   -> .b3fd5e52
  sidebar-box        -> .b3fd5e52
  masthead           -> .bc3527a6
  location-panel     -> .b3fd5e52
  component count: 4, generated rule count: 2
    .b3fd5e52 { border-radius: 4px; border: 1px solid #ccd; padding: 1.5rem }
    .bc3527a6 { border: 1px solid #ccd; padding: 1.5rem }

--- which approach guarantees what ---
approach                    name collision     leak-in    leak-out
naming convention alone        not blocked not blocked not blocked
generated local name               blocked not blocked     blocked
runtime generation                 blocked not blocked     blocked
document scope declaration     not blocked not blocked     blocked
shadow tree encapsulation          blocked     blocked     blocked
```

## Generated Local Names

In the first approach, short, readable names are written in the source files: `.card`,
`.heading`. A compile step hashes these names together with the file path into unique names,
and the template that produces the document uses the same mapping.

The output's first block shows two properties. The name `.card` in two separate files is
translated into two different names — collision is structurally impossible. The same input
always generates the same name; generation is **deterministic**, giving the same output across
two separate compiles.

Putting the local name at the front of the generated name is deliberate: seeing
`.card_574af` in developer tools tells you which source name it is. Names made of nothing but a
hash lose this readability.

The approach's limit is that a name **cannot be assembled at runtime**. If a name is produced
by string concatenation, the compiler cannot see that name and cannot map it; names must
appear as full literal text in the source.

## Sharing by Declaration Hash

In the second approach, the class name is derived from the declarations themselves. The
declaration set is sorted and reduced to a single piece of text, and that text's hash becomes
the class name.

The output's second block produces two rules for four components. Three components'
declarations are the same — the write order and extra whitespace differ, but the normalized
text is identical — and all three share the same class. The one component whose declaration
set differs gets its own class.

The result is that the generated file grows not with the number of components but with the
number of **distinct declaration sets**. Compared to the previous lesson's notation that
copies the same body, repetition disappears on its own.

Its cost falls into three categories. If generation happens at runtime, rules may be added to
the document after the first paint. The cascade order of rules depends on generation order and
cannot be predicted by reading the source. Hashed names are not readable; a debugging tool
shows the rule but not which component it came from.

## Document Scope Declaration

CSS's own tool is the `@scope` rule. It takes a **scoping root** and an optional **scoping
limit**; the rules inside it match only elements in the root's subtree, outside the limit.

```css
@scope (.measurement-card) to (.measurement-card__body) {
  p { margin-block: 0; }
  a { color: var(--brand-dark); }
}
```

The `p` rule here applies only inside the measurement card, and only before entering the
card's body. Written without scope, the same rule would match every paragraph on the page.

`@scope` brings one more novelty: between two scoped rules writing to the same property, the
one whose scoping root is **closer** to the element wins. This criterion is evaluated before
specificity. This is why, in two nested theme scopes, the inner theme wins.

The table's limit matters here: `@scope` blocks leak-out, it does not block name collision. If
two components use the same class name and both write unscoped rules, the problem persists.

## Shadow Tree Encapsulation

The one approach that blocks all three directions at once is the shadow tree. A stylesheet
inside a shadow root built with Web Components applies only to that tree; outside selectors do
not match inward, inside selectors do not match outward.

Encapsulation has three deliberate gaps. Inherited properties cross the boundary: font and
color flow from outside in. Custom properties are inherited too, so a component's style
interface is filled from outside through the same mechanism. The `::part` pseudo-element lets
the parts a component exposes outward be targeted.

These gaps are not a flaw, they are a design: full isolation would close the component off from
theming.

## The Selection Criterion

The approaches do not exclude each other, and the choice depends on three questions. Are
components written by separate teams — is name collision a real risk? Does style arrive on
the page from outside — is protection against leak-in needed? Is a compile step already in
place?

On a small page, a naming convention together with `@scope` is enough; neither needs a compile
step. Where components are distributed independently, generated names or a shadow tree are
needed.

## In the Station Page

```css
/* components/measurement-card.css — step 4: scoped notation */
@scope (.measurement-card) {
  :scope {
    display: grid;
    gap: var(--spacing-0);
    background-color: var(--card-surface, var(--surface));
  }

  .measurement-card__value { font-size: 2rem; font-variant-numeric: tabular-nums; }

  p  { margin-block: 0; }
  dt { color: var(--text-muted); }
}
```

The `:scope` pseudo-class targets the scoping root itself. Type selectors can now be written
with confidence: the `p` and `dt` rules apply only inside the card and do not leak into the
rest of the page.

Class names were still written methodically. Scope cuts off leak-out, but if another
`.measurement-card__value` is defined elsewhere on the page, the two rules still compete.

## Summary

- Style leakage is three separate events: name collision, leak-in, and leak-out; an approach
  may not block all three at once.
- Generated local names structurally prevent collision by hashing the local name together
  with the file path, and are deterministic; they work on the condition that the name is not
  assembled at runtime.
- Classes generated by declaration hash let components share the same declaration set; the
  generated file grows with the number of distinct declaration sets, not the number of
  components.
- `@scope` takes a root and a limit, blocks leak-out, and applies its proximity criterion
  before specificity; it does not prevent name collision.
- The shadow tree closes off all three directions; inherited properties, custom properties,
  and `::part` are deliberately left-open gateways.

## Next Step

Every approach up to this point shares the same assumption: style is written per component,
and each component has its own rules. There is an approach that argues the opposite —
never writing component-specific rules at all, building the document out of predefined,
single-declaration classes. The next lesson takes up this approach and the constraint
philosophy it rests on.
