From 135f1ec004352048454e732202c0bc8332816242 Mon Sep 17 00:00:00 2001
From: Simon H <5968653+dummdidumm@users.noreply.github.com>
Date: Tue, 25 Aug 2026 11:24:45 +0200
Subject: [PATCH] fix: more robust rendering of Svelte custom element slots
(#18710)
Instead of checking the given nodes at startup and only injecting those
into the inner Svelte component constructor, we are now detecting (via
the `$$host` property) whether or not the component is rendered as a
custom element, and in that case create all slots as real slots right
away.
Fixes #13638
Fixes #8997 (therefore closes #8999)
---
.changeset/open-parents-stare.md | 5 ++++
.../src/internal/client/dom/blocks/slot.js | 19 +++++++++++++
.../$$slot-dynamic-content/_config.js | 8 +++---
.../html-slots/_config.js | 28 +++++++++++++++----
.../no-shadow-dom/_config.js | 3 +-
.../no-shadow-dom/main.svelte | 1 +
6 files changed, 53 insertions(+), 11 deletions(-)
create mode 100644 .changeset/open-parents-stare.md
diff --git a/.changeset/open-parents-stare.md b/.changeset/open-parents-stare.md
new file mode 100644
index 0000000000..cb94267ea0
--- /dev/null
+++ b/.changeset/open-parents-stare.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: more robust rendering of Svelte custom element slots
diff --git a/packages/svelte/src/internal/client/dom/blocks/slot.js b/packages/svelte/src/internal/client/dom/blocks/slot.js
index 10b2959582..491f04a5ea 100644
--- a/packages/svelte/src/internal/client/dom/blocks/slot.js
+++ b/packages/svelte/src/internal/client/dom/blocks/slot.js
@@ -1,4 +1,6 @@
import { hydrate_next, hydrating } from '../hydration.js';
+import { create_element, create_text } from '../operations.js';
+import { append } from '../template.js';
/**
* @param {Comment} anchor
@@ -12,6 +14,23 @@ export function slot(anchor, $$props, name, slot_props, fallback_fn) {
hydrate_next();
}
+ // Custom element slots are native DOM slots.
+ // Use the stored reference because the shadow root may be closed.
+ if ($$props.$$host?.$$shadowRoot) {
+ const element = create_element('slot');
+ if (name !== 'default') element.name = name;
+
+ append(anchor, element);
+
+ if (fallback_fn !== null) {
+ const fallback_anchor = create_text();
+ element.append(fallback_anchor);
+ fallback_fn(fallback_anchor);
+ }
+
+ return;
+ }
+
var slot_fn = $$props.$$slots?.[name];
// Interop: Can use snippets to fill slots
var is_interop = false;
diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/$$slot-dynamic-content/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/$$slot-dynamic-content/_config.js
index 8ade347a79..3fd5a09888 100644
--- a/packages/svelte/tests/runtime-browser/custom-elements-samples/$$slot-dynamic-content/_config.js
+++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/$$slot-dynamic-content/_config.js
@@ -14,8 +14,8 @@ export default test({
assert.htmlEqual(
ce.shadowRoot.innerHTML,
`
-
named fallback
+named fallback
named fallback
+named fallback
default fallback content
'); + assert.equal(slot1.localName, 'slot'); + assert.equal(slot1.name, 'foo'); + assert.equal(slot1.assignedNodes().length, 0); + assert.equal(slot1.innerHTML, 'foo fallback content
'); + + const default_content = document.createElement('strong'); + default_content.textContent = 'default content'; + el.append(default_content); + + const named_content = document.createElement('strong'); + named_content.slot = 'foo'; + named_content.textContent = 'named content'; + el.append(named_content); + + assert.equal(slot0.assignedNodes().length, 1); + assert.equal(slot0.assignedNodes()[0], default_content); + assert.equal(slot1.assignedNodes().length, 1); + assert.equal(slot1.assignedNodes()[0], named_content); } }); diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/no-shadow-dom/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/no-shadow-dom/_config.js index cd8550120a..e83491418a 100644 --- a/packages/svelte/tests/runtime-browser/custom-elements-samples/no-shadow-dom/_config.js +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/no-shadow-dom/_config.js @@ -3,7 +3,7 @@ const tick = () => Promise.resolve(); export default test({ async test({ assert, target }) { - target.innerHTML = '