Skip to content

Commit b012c38

Browse files
committed
fix: inject DSD with inline styles for nested custom elements
The SSR pipeline's injectDSD() was not recursively processing custom elements nested inside other components' shadow DOM. A <theme-toggle> inside <blog-shell>'s render output was emitted as an empty element with no DSD template — no styles, no content until JS upgraded it. This caused a visible layout shift (CLS) in the header. Changes: - render-server.js: recursively call injectDSD() on each component's rendered inner HTML so nested elements get their own DSD templates with inline <style> tags. Mirrors how Lit SSR handles this. - component.js: when a shadow root already exists from DSD, remove the SSR inline <style> before calling adoptStyles() to avoid duplicate styles (adoptedStyleSheets takes over). - blog-shell.ts: revert the manual CLS workaround — no longer needed since the framework now handles it.
1 parent 606e980 commit b012c38

3 files changed

Lines changed: 15 additions & 10 deletions

File tree

examples/blog/components/blog-shell.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,6 @@ export class BlogShell extends WebComponent {
6161
}
6262
.nav a:hover { color: var(--fg); }
6363
64-
/* Reserve space for theme-toggle before it upgrades to prevent
65-
header height shift (CLS). Matches the button size in theme-toggle. */
66-
.nav theme-toggle {
67-
display: inline-flex;
68-
width: 36px;
69-
height: 36px;
70-
}
71-
7264
main {
7365
display: block;
7466
max-width: 760px;

packages/core/src/component.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,13 +350,23 @@ export class WebComponent extends Base {
350350
this._connected = true;
351351
const Ctor = /** @type any */ (this.constructor);
352352
if (Ctor.shadow !== false) {
353+
const hadSSRShadow = !!this.shadowRoot;
353354
if (!this.shadowRoot) {
354355
/** @type any */ (this).attachShadow({ mode: 'open' });
355356
}
356357
this._renderRoot = this.shadowRoot;
357358
const styles = Ctor.styles;
358359
const list = Array.isArray(styles) ? styles : isCSS(styles) ? [styles] : [];
359-
if (list.length) adoptStyles(this._renderRoot, list);
360+
if (list.length) {
361+
// If the shadow root came from Declarative Shadow DOM (SSR), it
362+
// contains an inline <style> tag. Remove it before switching to
363+
// adoptedStyleSheets to avoid duplicate styles.
364+
if (hadSSRShadow) {
365+
const ssrStyle = this.shadowRoot.querySelector('style');
366+
if (ssrStyle) ssrStyle.remove();
367+
}
368+
adoptStyles(this._renderRoot, list);
369+
}
360370
} else {
361371
this._renderRoot = this;
362372
// Light DOM: static styles is not supported (no shadow root for

packages/core/src/render-server.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,10 @@ async function injectDSD(html, ctx) {
254254
applyAttrsToInstance(instance, attrMap, Cls);
255255
let tpl = instance.render ? instance.render() : '';
256256
if (tpl && typeof tpl.then === 'function') tpl = await tpl;
257-
const inner = await render(tpl, ctx);
257+
// Render the template to HTML, then recursively inject DSD for
258+
// any nested custom elements (e.g. <theme-toggle> inside <blog-shell>).
259+
const rawInner = await render(tpl, ctx);
260+
const inner = await injectDSD(rawInner, ctx);
258261

259262
if (isShadow) {
260263
// Shadow DOM: wrap in Declarative Shadow DOM template

0 commit comments

Comments
 (0)