diff --git a/.changeset/sharp-shoes-look.md b/.changeset/sharp-shoes-look.md
new file mode 100644
index 000000000000..2caf6d421b99
--- /dev/null
+++ b/.changeset/sharp-shoes-look.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: take into account registration state when setting custom element props
diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js
index 0276069eee49..8f6588e94aa1 100644
--- a/packages/svelte/src/internal/client/dom/elements/attributes.js
+++ b/packages/svelte/src/internal/client/dom/elements/attributes.js
@@ -201,7 +201,7 @@ export function set_xlink_attribute(dom, attribute, value) {
}
/**
- * @param {any} node
+ * @param {HTMLElement} node
* @param {string} prop
* @param {any} value
*/
@@ -216,10 +216,21 @@ export function set_custom_element_data(node, prop, value) {
set_active_reaction(null);
set_active_effect(null);
try {
- if (get_setters(node).includes(prop)) {
+ if (
+ // Don't compute setters for custom elements while they aren't registered yet,
+ // because during their upgrade/instantiation they might add more setters.
+ // Instead, fall back to a simple "an object, then set as property" heuristic.
+ setters_cache.has(node.nodeName) || customElements.get(node.tagName.toLowerCase())
+ ? get_setters(node).includes(prop)
+ : value && typeof value === 'object'
+ ) {
+ // @ts-expect-error
node[prop] = value;
} else {
- set_attribute(node, prop, value);
+ // We did getters etc checks already, stringify before passing to set_attribute
+ // to ensure it doesn't invoke the same logic again, and potentially populating
+ // the setters cache too early.
+ set_attribute(node, prop, value == null ? value : String(value));
}
} finally {
set_active_reaction(previous_reaction);
@@ -384,8 +395,9 @@ function get_setters(element) {
var setters = setters_cache.get(element.nodeName);
if (setters) return setters;
setters_cache.set(element.nodeName, (setters = []));
+
var descriptors;
- var proto = get_prototype_of(element);
+ var proto = element; // In the case of custom elements there might be setters on the instance
var element_proto = Element.prototype;
// Stop at Element, from there on there's only unnecessary setters we're not interested in
diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/late-ce-mount/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/late-ce-mount/_config.js
new file mode 100644
index 000000000000..0679c820ba8c
--- /dev/null
+++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/late-ce-mount/_config.js
@@ -0,0 +1,25 @@
+import { flushSync } from 'svelte';
+import { test } from '../../assert';
+
+const tick = () => Promise.resolve();
+
+// Check that rendering a custom element and setting a property before it is registered
+// does not break the "when to set this as a property" logic
+export default test({
+ async test({ assert, target }) {
+ target.innerHTML = '';
+ await tick();
+ await tick();
+
+ const ce_root = target.querySelector('custom-element').shadowRoot;
+
+ ce_root.querySelector('button')?.click();
+ flushSync();
+ await tick();
+ await tick();
+
+ const inner_ce_root = ce_root.querySelectorAll('set-property-before-mounted');
+ assert.htmlEqual(inner_ce_root[0].shadowRoot.innerHTML, 'object|{"foo":"bar"}');
+ assert.htmlEqual(inner_ce_root[1].shadowRoot.innerHTML, 'object|{"foo":"bar"}');
+ }
+});
diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/late-ce-mount/main.svelte b/packages/svelte/tests/runtime-browser/custom-elements-samples/late-ce-mount/main.svelte
new file mode 100644
index 000000000000..09a139e642b1
--- /dev/null
+++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/late-ce-mount/main.svelte
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+{#if property}
+
+{/if}