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 get the same class names.
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.
const [{ card: cardOne }, rulesOne] = css`
.card {
background: ${$c("bg")};
padding: ${$s("padding")};
}
`; <div class={cardOne}>This card gets the bg background.</div>const [{ card: cardTwo }, rulesTwo] = css`
.card {
background: ${$c("accent")};
color: ${$c("bg")};
padding: ${$s("padding")};
}
`; <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:
classMap— object mapping camelCase class name → hashed class name (e.g.{ fooBar: "foo-bar_abc1234" }). Use these values inclassorclass:listattributes.hashedRules—CSSRule[]with renamed selectors. Pass to<Style>.
<Style rules={CSSRule[]} />
Renders a raw <style> tag from a CSSRule[].
Inline <Style> vs. the stylesheet
<Style> writes its <style> tag where the component renders, every time it
renders. The CSS only reaches pages that use the component, but it is never
cached between pages, and a component rendered ten times on a page emits the
same block ten times.
Rules that don’t depend on props can go in the generated stylesheet instead.
Define them in a .ts module and return them from a preflight. That’s how
DDI’s own components ship their CSS: appRules.componentRules, in the
components layer of ddi.config.ts. The stylesheet is downloaded once and
cached, but it carries the rules on every page, used or not. Use <Style>
for rules that are computed per render, or that only a few pages need.
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.