From 38098a931142dfef6ca7330d33264d1908b698fa Mon Sep 17 00:00:00 2001
From: Mariana Castro <121824373+maricastroc@users.noreply.github.com>
Date: Wed, 23 Sep 2026 13:58:28 -0300
Subject: [PATCH] fix: don't overwrite an unchanged spread `value` (#18864)
Fixes #18862
When a `value` is applied through a spread (``),
`set_attributes` always writes `element.value`, even if the element
already has that value. Writing `value` on a number input clears any
incomplete text: typing `250.` makes `input.value` `''` (the text is
still visible, but `validity.badInput` is `true`). If the spread `value`
then becomes `''` too, writing `''` erases what the user typed.
This is what happens with SvelteKit remote forms:
`fields.quantity.as('number')` coerces `''` to `undefined`, the spread
`value` becomes `''`, and Svelte writes it back to the input. When `.`
is typed:
- Chromium clears the input in every locale, because `250.` is always
incomplete input there.
- WebKit/Safari clears it in locales where `.` is not the decimal
separator (e.g. `fr-CH`), which is where the reporter saw it.
A plain `` never clears. Neither does the same
`value` in Svelte without a spread (`value={...}`), because `set_value`
already skips writing when `element.value === value`. This PR adds the
same check to the spread path. Svelte 3 had this check too (#3426,
#3495); it was lost in Svelte 5.
The first write still always happens. Writing `value` sets the input's
dirty value flag, and without that flag a `defaultValue` applied
afterwards would change the current value (covered by
`form-default-value-spread`).
This also likely affects sveltejs/kit#16270 (same symptom, plus a cursor
jump caused by redundant writes). That issue is handled on the kit side
in sveltejs/kit#16320; this PR is independent of it.
The test runs in a real browser (`runtime-browser`). It types with
`document.execCommand('insertText')`, which goes through Chromium's own
editing, so `250.` is real incomplete input. The locale is not mocked.
The test fails without this change and passes with it.
### Before submitting the PR, please make sure you do the following
- [x] It's really useful if your PR references an issue where it is
discussed ahead of time. In many cases, features are absent for a
reason. For large changes, please create an RFC:
https://github.com/sveltejs/rfcs
- [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`.
- [x] This message body should clearly illustrate what problems it
solves.
- [x] Ideally, include a test that fails without this PR but passes with
it.
- [x] If this PR changes code within `packages/svelte/src`, add a
changeset (`npx changeset`).
### Tests and linting
- [x] Run the tests with `pnpm test` and lint the project with `pnpm
lint`
---------
Co-authored-by: Paolo Ricciuti
---
.changeset/quiet-inputs-keep.md | 5 ++++
.../client/dom/elements/attributes.js | 16 ++++++++++-
.../_config.js | 28 +++++++++++++++++++
.../main.svelte | 5 ++++
4 files changed, 53 insertions(+), 1 deletion(-)
create mode 100644 .changeset/quiet-inputs-keep.md
create mode 100644 packages/svelte/tests/runtime-browser/samples/spread-input-number-incomplete-value/_config.js
create mode 100644 packages/svelte/tests/runtime-browser/samples/spread-input-number-incomplete-value/main.svelte
diff --git a/.changeset/quiet-inputs-keep.md b/.changeset/quiet-inputs-keep.md
new file mode 100644
index 0000000000..0efcfcc058
--- /dev/null
+++ b/.changeset/quiet-inputs-keep.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: don't overwrite an unchanged spread `value`, preserving incomplete number input
diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js
index 0fe1b8b2d3..7d2dd62baf 100644
--- a/packages/svelte/src/internal/client/dom/elements/attributes.js
+++ b/packages/svelte/src/internal/client/dom/elements/attributes.js
@@ -422,7 +422,21 @@ function set_attributes(
} else if (!is_custom_element && (key === '__value' || (key === 'value' && value != null))) {
// @ts-ignore We're not running this for custom elements because __value is actually
// how Lit stores the current value on the element, and messing with that would break things.
- element.value = element.__value = value;
+ element.__value = value;
+ // we don't set the value if it hasn't changed. This supports invalid number inputs like `1e` because
+ // 1. user types 1e
+ // 2. the state is updated reading e.target.value which is ''
+ // 3. the spreaded value is ''
+ // 4. updating input.value would thus, clear the user value
+ if (
+ prev_value == null ||
+ // @ts-ignore
+ element.value !== value ||
+ (value === 0 && element.nodeName === PROGRESS_TAG)
+ ) {
+ // @ts-ignore
+ element.value = value;
+ }
} else if (key === 'selected' && is_option_element) {
set_selected(/** @type {HTMLOptionElement} */ (element), value);
} else {
diff --git a/packages/svelte/tests/runtime-browser/samples/spread-input-number-incomplete-value/_config.js b/packages/svelte/tests/runtime-browser/samples/spread-input-number-incomplete-value/_config.js
new file mode 100644
index 0000000000..cec1e179ac
--- /dev/null
+++ b/packages/svelte/tests/runtime-browser/samples/spread-input-number-incomplete-value/_config.js
@@ -0,0 +1,28 @@
+import { flushSync } from 'svelte';
+import { ok, test } from '../../assert';
+
+export default test({
+ async test({ assert, target }) {
+ const input = target.querySelector('input');
+ ok(input);
+
+ input.focus();
+
+ // we need to use `document.execCommand('insertText', false, ...)` to simulate user input
+ // because directly setting an invalid value to `input.value` would simply clear the input
+ // and dispatching an event would not update the input correctly
+ document.execCommand('insertText', false, '1');
+ flushSync();
+ // `1e` is incomplete on every platform, unlike `1.` which Chromium on Linux accepts as `1`
+ document.execCommand('insertText', false, 'e');
+ flushSync();
+
+ assert.equal(input.value, '');
+ assert.equal(input.validity.badInput, true);
+
+ document.execCommand('insertText', false, '5');
+ flushSync();
+
+ assert.equal(input.value, '1e5');
+ }
+});
diff --git a/packages/svelte/tests/runtime-browser/samples/spread-input-number-incomplete-value/main.svelte b/packages/svelte/tests/runtime-browser/samples/spread-input-number-incomplete-value/main.svelte
new file mode 100644
index 0000000000..3e7a5cc335
--- /dev/null
+++ b/packages/svelte/tests/runtime-browser/samples/spread-input-number-incomplete-value/main.svelte
@@ -0,0 +1,5 @@
+
+
+ (value = e.currentTarget.value)} />