diff --git a/docs/language-charter.md b/docs/language-charter.md
index 3a98164a..4b4899db 100644
--- a/docs/language-charter.md
+++ b/docs/language-charter.md
@@ -1338,9 +1338,12 @@ component Card:
```
- Component CSS is scoped by default.
+- A scoped selector may style a child component's top-level DOM root. The
+ parent's scope marker is attached only to that root (or each top-level root
+ in a fragment), so arbitrary DOM inside the child remains private.
- Production builds extract static CSS.
- Global CSS requires `style global:`.
-- Velar Web does not create a CSS-in-JS object system.
+- VelarScript Web does not create a CSS-in-JS object system.
## 21. Velar Compiler
diff --git a/packages/web/src/emitter.ts b/packages/web/src/emitter.ts
index 258db073..fd487b8f 100644
--- a/packages/web/src/emitter.ts
+++ b/packages/web/src/emitter.ts
@@ -282,7 +282,8 @@ export class WebJavaScriptEmitter extends JavaScriptEmitter {
if (hasMeaningfulChildren(expression.children)) properties.push(`children: ${this.emitJsxChildren(expression.children, scope, namespace)}`);
const props = properties.join(", ");
const component = `${expression.tag}({ ${props} }, ${namespace})`;
- return asChild ? `__velarUseComponent(${component}, ${scope})` : component;
+ const parentStyleScope = this.currentStyleScope ? `, ${JSON.stringify(this.currentStyleScope)}` : "";
+ return asChild ? `__velarUseComponent(${component}, ${scope}${parentStyleScope})` : component;
}
const id = ++this.jsxId;
@@ -863,7 +864,19 @@ function __velarComponent(node, scope, mounted, cleanup) {
};
}
-function __velarUseComponent(instance, scope) {
+function __velarScopeComponentRoot(node, attribute) {
+ if (!attribute || !node) return;
+ if (node.nodeType === 1) {
+ node.setAttribute(attribute, "");
+ return;
+ }
+ if (node.nodeType === 11) {
+ for (const child of node.childNodes) if (child.nodeType === 1) child.setAttribute(attribute, "");
+ }
+}
+
+function __velarUseComponent(instance, scope, parentStyleScope = "") {
+ __velarScopeComponentRoot(instance.node, parentStyleScope);
scope.mounts.push(() => instance.__mount());
scope.cleanups.push(() => instance.destroy(false));
return instance.node;
diff --git a/tests/browser.acceptance.ts b/tests/browser.acceptance.ts
index 8f02647b..dc13f673 100644
--- a/tests/browser.acceptance.ts
+++ b/tests/browser.acceptance.ts
@@ -152,6 +152,7 @@ async function acceptBrowser(name: string, browserType: BrowserType, baseUrl: st
assert.equal(await page.locator("[data-route]").textContent(), "/");
assert.match(await page.locator("[data-environment]").textContent() ?? "", /^online\/visible\/(?:dark|light)$/u);
assert.equal(await page.locator("[data-session]").textContent(), "session-ready");
+ await page.waitForFunction(() => document.querySelector("[data-frame]")?.textContent === "frame-ready");
assert.equal(await page.locator("[data-frame]").textContent(), "frame-ready");
assert.equal(await page.locator("[data-layout]").textContent(), "measured");
assert.equal(await page.locator("[data-error]").textContent(), "Velar recovery ready");
diff --git a/tests/compiler.test.ts b/tests/compiler.test.ts
index f2e0c201..add9b59a 100644
--- a/tests/compiler.test.ts
+++ b/tests/compiler.test.ts
@@ -7924,6 +7924,28 @@ mount(, "#app")
assert.ok(domCommit >= 0 && watchCommit > domCommit);
});
+test("scoped CSS can style the top-level DOM root of a child component", () => {
+ const result = compile(`
+component ChildLink:
+ return Docs
+
+component Navigation:
+ style:
+ nav a {
+ text-decoration: none;
+ }
+
+ return
+`.trimStart());
+
+ assert.deepEqual(result.diagnostics, []);
+ const scope = result.css?.match(/nav\[(data-velar-[a-z0-9]+)\] a\[\1\]/)?.[1];
+ assert.ok(scope);
+ assert.match(result.code ?? "", new RegExp(`__velarUseComponent\\(ChildLink\\(\\{ \\}, __namespace\\), __childScope, "${scope}"\\)`));
+ assert.match(result.code ?? "", /function __velarScopeComponentRoot\(node, attribute\)/u);
+ assert.match(result.code ?? "", /node\.nodeType === 11/u);
+});
+
test("component resources own typed asynchronous loading, retry, errors, and stale completion", () => {
const result = compile(`
async def loadLabel() -> string: