Repository navigation
fix(core): preserve nested-rule selectors in composition CSS scoping (#2721) - #2733
Merged
Merged
Conversation
…2721) Chrome 112+ / Firefox 117+ / Safari 16.5+ support native CSS Nesting. A nested rule like '.title { … }' inside '[data-composition-id="intro"] { … }' resolves at match time to '<parent> .title' via the implicit '&' prefix. 'scopeCssToComposition' walks every rule via 'root.walkRules' and re- scopes selectors, but it did so for nested rules too — producing '[…scope…] .title' inside '[…scope…] { … }', which nesting then prepends AGAIN to '[…scope…] […scope…] .title'. Since the composition root only appears once in the DOM, the doubly-scoped selector never matches — the nested rule appears 'just ignored' as the reporter described (#2721). Reproduced on 0.7.66 with the reporter's exact composition. Fix: add 'isNestedInsideAnotherRule' predicate — mirrors the existing 'isInsideGlobalAtRule' — and skip nested rules in the walkRules callback. Top-level rules still get scoped; their nested descendants inherit scope naturally via CSS Nesting at match time. Added two focused tests: - 'preserves nested-rule selectors so CSS Nesting inheritance works (#2721)' — asserts nested '.title' and 'h2' selectors stay verbatim while top-level rules keep scoping. - 'preserves deeply-nested CSS Nesting rules (#2721)' — same rule at depth 3. All 37 existing scopeCssToComposition tests still pass. Fixes #2721. Co-Authored-By: Claude Opus 4.7 <[email protected]>
miguel-heygen
approved these changes
Jul 22, 2026
miguel-heygen
left a comment
Collaborator
There was a problem hiding this comment.
Clean fix at exact head 34501006b72822defb4586d8ef8e172416c68573.
Strengths
packages/core/src/compiler/compositionScoping.ts:212–:237walks the full ancestor chain, including intervening group at-rules, and scopes only the outer style rule; nested selectors therefore inherit the single parent scope exactly as native CSS Nesting specifies.packages/core/src/compiler/compositionScoping.test.ts:803–:845pins the reporter case and a three-deep nesting case while retaining the existing top-level/global-at-rule suite.
Verification
- Focused suite: 37/37 pass.
git diff --checkpasses.- Chromium probe on the exact shape: top-level
h1computesorangered, nested.titlecomputesbrown, and a nested@mediachild computesblue; the serialized CSS keeps nested selectors verbatim and scopes the parent once. - GitHub Build, Typecheck, and runtime-contract are green; remaining required Test/Windows lanes are still running, not failing.
Verdict: APPROVE
Reasoning: The ancestor-rule guard fixes the double-scope root cause without disturbing top-level or global at-rule handling, and both AST-level and browser-level evidence confirm the intended match behavior.
— Magi
dahans-msft2
pushed a commit
to dahans-msft2/hyperframes
that referenced
this pull request
Aug 6, 2026
…eygen-com#2721) (heygen-com#2733) Chrome 112+ / Firefox 117+ / Safari 16.5+ support native CSS Nesting. A nested rule like '.title { … }' inside '[data-composition-id="intro"] { … }' resolves at match time to '<parent> .title' via the implicit '&' prefix. 'scopeCssToComposition' walks every rule via 'root.walkRules' and re- scopes selectors, but it did so for nested rules too — producing '[…scope…] .title' inside '[…scope…] { … }', which nesting then prepends AGAIN to '[…scope…] […scope…] .title'. Since the composition root only appears once in the DOM, the doubly-scoped selector never matches — the nested rule appears 'just ignored' as the reporter described (heygen-com#2721). Reproduced on 0.7.66 with the reporter's exact composition. Fix: add 'isNestedInsideAnotherRule' predicate — mirrors the existing 'isInsideGlobalAtRule' — and skip nested rules in the walkRules callback. Top-level rules still get scoped; their nested descendants inherit scope naturally via CSS Nesting at match time. Added two focused tests: - 'preserves nested-rule selectors so CSS Nesting inheritance works (heygen-com#2721)' — asserts nested '.title' and 'h2' selectors stay verbatim while top-level rules keep scoping. - 'preserves deeply-nested CSS Nesting rules (heygen-com#2721)' — same rule at depth 3. All 37 existing scopeCssToComposition tests still pass. Fixes heygen-com#2721. Co-authored-by: Claude Opus 4.7 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Fixes #2721. External-user report from qd3v: a nested CSS rule inside a composition's
<style>block gets silently ignored.Reproducer (from the issue, exact):
The
h1gets styled; the.titledoesn't.Root cause
Modern browsers support native CSS Nesting (Chrome 112+, Firefox 117+, Safari 16.5+). A nested rule
.title { … }inside[data-composition-id="intro-comp"] { … }resolves at match time to<parent> .titlevia the implicit&prefix.scopeCssToCompositioninpackages/core/src/compiler/compositionScoping.tswalks every Rule viaroot.walkRulesand re-scopes selectors:PostCSS's
walkRulesvisits nested rules too. The nested.titleselector was scoped to[data-composition-id="intro-comp"] .title— but the parent's selector was also scoped, so when CSS Nesting resolved the inner rule it prepended&(the parent scope) AGAIN, producing effectively:The composition root only appears once in the DOM, so
<scope> <scope> .titlematches nothing — the rule appears "just ignored" as the reporter described.Fix
Add
isNestedInsideAnotherRulepredicate — mirrors the existingisInsideGlobalAtRule— and skip nested Rules in the walkRules callback. Top-level rules still get scoped; their nested descendants inherit scope naturally via CSS Nesting at match time.root.walkRules((rule) => { if (isInsideGlobalAtRule(rule)) return; + if (isNestedInsideAnotherRule(rule)) return; rule.selectors = rule.selectors.map((selector) => scopeSelector(…)); });Testing
bunx vitest run packages/core/src/compiler/compositionScoping.test.ts— 37 tests pass (35 pre-existing + 2 new).preserves nested-rule selectors so CSS Nesting inheritance works (#2721)— asserts nested.titleandh2selectors stay verbatim while top-level rules keep scoping, and no[…] .titlefalse-scoped selector appears.preserves deeply-nested CSS Nesting rules (#2721)— same guarantee at depth 3.root.walkRulesbit; reproduced the double-scoping via a local postcss probe before writing the fix.Note on other issues raised in the ticket
The reporter also mentioned two secondary points that are out of scope for this PR:
bodytag instead oftemplate, the rendering fails" — that's the standalone sub-composition contract (must have a<template>or<body>with[data-composition-id]). The error is correct; the docs could be clearer. Filing separately if it needs a doc update.data-layout-allow-overlap, but there's no such attribute in the docs" — that's a docs gap onhyperframes.heygen.com/concepts/data-attributes. Filing separately if needed.— Rames Jusso