DDI

Scoped CSS Rules

This page was updated in v2.11.0

Scoped rules hash a block of CSS and rename every class selector by appending a short deterministic suffix: .foo becomes .foo_<hash>. You get back a tuple of a class-name map and the renamed rules.

Because the suffix is derived from the rule content, identical rules always produce the same class names. Styles are stable across builds and two components with the same rules share one <style> block naturally.

You author these rules with the css`…` tagged template — plain CSS text that compiles to the same [classMap, rules] tuple the underlying scopedRules() primitive returns (see When to reach for scopedRules).

Usage

---
import { Style, css } from "@dynamic-type/ddi/server";

const [{ card }, rules] = css`
    .card {
        background: var(--ddi-c-bg-alt);
        padding: var(--ddi-s-padding);
    }
`;
---

<p class={card}>Styled content</p>

<Style rules={rules} />

The emitted HTML looks like:

<p class="card_1g91grm">Styled content</p>
<style>
    .card_1g91grm {
        background: var(--ddi-c-bg-alt);
        padding: var(--ddi-s-padding);
    }
</style>

Selectors with multiple classes, compound selectors, and descendant selectors all work — every .identifier token in the selector is renamed:

// .foo.bar  →  .foo_abc.bar_abc
// div.foo   →  div.foo_abc
// .foo .bar →  .foo_abc .bar_abc

Nested rules using & are left as-is — they are already scoped by the renamed parent selector.

Utility classes in a block (u:)

The u: pseudo-property is the in-block form of DDI’s utility-class shorthand — the same space-separated string you’d put in a class="..." attribute, resolved to CSS properties and spliced into the block in place:

---
import { Style, css } from "@dynamic-type/ddi/server";

const [{ card }, rules] = css`
    .card {
        u: bbe:subtle p:m;
        border-radius: var(--ddi-s-border-radius);
    }
`;
---

<div class={card}>Content</div>

<Style rules={rules} />

Quotes around the value are optional (u: p:m and u: "p:m" parse identically), multiple u: lines per block are allowed, and source order is preserved. If two utility classes set the same CSS property, the later one wins:

// u: p:m p:s  →  padding: var(--ddi-s-s)

An unrecognized class throws, so a typo’d utility class fails loudly instead of silently producing incomplete CSS.

Interpolation

Interpolation (${…}) is allowed only inside a block, in a property or value position — an interpolation in a selector throws. Keep interpolated values primitive:

const [{card}, rules] = css` .card { color: ${accent}; } `;

A property that appears twice in a block is emitted twice (last one wins in the cascade) — handy for progressive-enhancement fallbacks:

// .box { width: 100%; width: fit-content; }
// → both declarations emitted, in order

When to reach for scopedRules

css`…` targets the static case. The css tag compiles to scopedRules(rules: CSSRule[]) — the underlying primitive, which takes an array of rule(selector, properties) objects instead of CSS text:

---
import { rule, $c, scopedRules, Style } from "@dynamic-type/ddi/server";

const [{ card }, rules] = scopedRules([
    rule(".card", {
        background: $c("bg-alt"),
        padding: "var(--ddi-s-padding)",
    }),
]);
---

<p class={card}>Styled content</p>

<Style rules={rules} />

Both forms return the identical [classMap, rules] tuple and hash the same, so the class names match byte-for-byte. Reach for the object form when you need something the flat CSS text can’t express: conditionals, value: false property omission, or programmatically built media queries.

Isolation demo

Both fixtures below contain a .card element styled differently. Each css block produces a unique hash because the rules differ — so the class names don’t collide.

Scope one — bg-alt background
This card gets the bg background.
src/pages/_scoped-rules/IsolationDemo.astro  5–10
const [{ card: cardOne }, rulesOne] = css`
    .card {
        background: ${$c("bg")};
        padding: ${$s("padding")};
    }
`;
src/pages/_scoped-rules/IsolationDemo.astro  39–39
        <div class={cardOne}>This card gets the bg background.</div>
Scope two — accent background
This card gets the accent background.
src/pages/_scoped-rules/IsolationDemo.astro  12–18
const [{ card: cardTwo }, rulesTwo] = css`
    .card {
        background: ${$c("accent")};
        color: ${$c("bg")};
        padding: ${$s("padding")};
    }
`;
src/pages/_scoped-rules/IsolationDemo.astro  62–62
        <div class={cardTwo}>This card gets the accent background.</div>

API

css`…` — Tagged-template form. Parses a static CSS block (with the u: pseudo-property and value-position ${…} interpolation) and returns [Record<string, string>, CSSRule[]]. Compiles to scopedRules; for dynamic/conditional styles, use scopedRules directly.

scopedRules(rules: CSSRule[]): [Record<string, string>, CSSRule[]]

Returns a tuple:

<Style rules={CSSRule[]} />

Renders a raw <style> tag from a CSSRule[].

u(classNames: string): Record<string, string>

Resolves a space-separated utility-class string to a CSS properties object — the object-form equivalent of the u: pseudo-property. Throws if any class doesn’t match a known rule.