diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js
index e1c3d805dc..ec45e8cf0c 100644
--- a/packages/svelte/src/internal/client/dom/elements/attributes.js
+++ b/packages/svelte/src/internal/client/dom/elements/attributes.js
@@ -472,6 +472,16 @@ function set_attributes(
element.__value = null;
}
}
+ } else if (is_default && renderer != null) {
+ // Route through the renderer-aware abstraction so custom renderers
+ // see defaultValue/defaultChecked as proper attributes
+ if (name === 'defaultValue') {
+ set_element_default_value(element, value);
+ } else {
+ set_element_default_checked(element, value);
+ }
+ // remove it from attributes's cache
+ if (name in attributes) attributes[name] = UNINITIALIZED;
} else if (
is_default ||
(setters.includes(name) && (is_custom_element || typeof value !== 'string'))
diff --git a/packages/svelte/tests/custom-renderers/samples/default-value-spread/_config.js b/packages/svelte/tests/custom-renderers/samples/default-value-spread/_config.js
new file mode 100644
index 0000000000..e5fd87d389
--- /dev/null
+++ b/packages/svelte/tests/custom-renderers/samples/default-value-spread/_config.js
@@ -0,0 +1,79 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ test({ assert, target, dispatch_event }) {
+ const inputs = target.children.filter(
+ (/** @type {any} */ n) => n.type === 'element' && n.name === 'input'
+ );
+ const button = target.children.find(
+ (/** @type {any} */ n) => n.type === 'element' && n.name === 'button'
+ );
+
+ assert.equal(inputs.length, 4);
+ assert.ok(button);
+
+ // Input 1: direct defaultValue attribute
+ const input_direct_val = inputs[0];
+ // Input 2: defaultValue via spread
+ const input_spread_val = inputs[1];
+ // Input 3: direct defaultChecked attribute (with `checked` alongside for consistent codegen)
+ const input_direct_chk = inputs[2];
+ // Input 4: defaultChecked via spread
+ const input_spread_chk = inputs[3];
+
+ // --- Initial state ---
+
+ // Direct defaultValue should appear as a renderer attribute
+ assert.equal(input_direct_val.attributes['value'], 'fixed');
+ assert.equal(input_direct_val.attributes['defaultValue'], 'default_val');
+
+ // Spread defaultValue should produce the SAME result as direct defaultValue.
+ // BUG: without fix, spread bypasses the renderer and writes element.defaultValue directly
+ // so the attribute won't exist on the object-based renderer node
+ assert.equal(input_spread_val.attributes['value'], 'fixed');
+ assert.equal(
+ input_spread_val.attributes['defaultValue'],
+ 'default_val',
+ 'defaultValue via spread should go through renderer.setAttribute, not element.defaultValue'
+ );
+
+ // Direct defaultChecked should appear as a renderer attribute
+ assert.equal(input_direct_chk.attributes['type'], 'checkbox');
+ assert.equal(input_direct_chk.attributes['checked'], '');
+ assert.equal(input_direct_chk.attributes['defaultChecked'], '');
+
+ // Spread defaultChecked should also go through the renderer API.
+ // set_element_default_checked treats true as a boolean attribute (empty string).
+ assert.equal(input_spread_chk.attributes['type'], 'checkbox');
+ assert.equal(
+ input_spread_chk.attributes['defaultChecked'],
+ '',
+ 'defaultChecked via spread should go through renderer.setAttribute, not element.defaultChecked'
+ );
+
+ // --- After update ---
+ dispatch_event(button, 'click');
+ flushSync();
+
+ // Direct defaultValue should update
+ assert.equal(input_direct_val.attributes['defaultValue'], 'new_default');
+
+ // Spread defaultValue should also update via renderer
+ assert.equal(
+ input_spread_val.attributes['defaultValue'],
+ 'new_default',
+ 'updated defaultValue via spread should go through renderer.setAttribute'
+ );
+
+ // Direct defaultChecked should be removed (false)
+ assert.equal(input_direct_chk.attributes['defaultChecked'], undefined);
+
+ // Spread defaultChecked should also be removed via renderer
+ assert.equal(
+ input_spread_chk.attributes['defaultChecked'],
+ undefined,
+ 'updated defaultChecked=false via spread should go through renderer.removeAttribute'
+ );
+ }
+});
diff --git a/packages/svelte/tests/custom-renderers/samples/default-value-spread/main.svelte b/packages/svelte/tests/custom-renderers/samples/default-value-spread/main.svelte
new file mode 100644
index 0000000000..c24601749e
--- /dev/null
+++ b/packages/svelte/tests/custom-renderers/samples/default-value-spread/main.svelte
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+