Repository navigation
Description
I was testing the reproduction from solidjs/solid#1754 in the Solid 2 Playground.
import { createSignal } from "solid-js";
import { render } from "@solidjs/web";
function App() {
const [selected] = createSignal("2");
return (
<div>
<select value="2">
<option value="" />
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
<select value={selected()}>
{(() => (
<>
<option value="" />
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</>
)) as unknown as Element}
</select>
<select {...{}} value={selected()}>
{(() => (
<>
<option value="" />
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</>
)) as unknown as Element}
</select>
</div>
);
}
if (typeof document !== "undefined") {
render(() => <App />, document.getElementById("root")!);
}On Solid 2 RC.4, I get:
value="2"→ empty optionvalue={selected()}→2{...{}} value={selected()}→2
The static form appears in the generated DOM as:
<select value="2">Browsers do not use a value attribute on <select> to determine its current selection. The reactive forms appear to assign the live select.value property instead.
For comparison, the equivalent Svelte 5 code displays 2, 2, 2:
<script lang="ts">
let selected = $state("2");
</script>
<div>
<select value="2">
<option value=""></option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
<select value={selected}>
<option value=""></option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
<select {...{}} value={selected}>
<option value=""></option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
</div>I understand that the first select may originally have been included in #1754 as a raw-browser-behavior baseline. However, in Solid 2, should these equivalent JSX values produce different results?
<select value="2">
<select value={selected()}>Is the static behavior intentional, or should the compiler also assign the value property after creating the options?
The spread-ordering problem from #1754 appears resolved in RC.4, so this seems like a separate static-literal compiler question.
Activity
nickshiro commentedon Sep 1, 2026
Hi, I'd like to work on this.
I traced the difference to the static attribute path: a literal select value is inlined into the HTML template, while reactive and spread values go through
setAttr()and assign the live value property.Plan: route only static select.value through the existing property-assignment path, without changing other locked DOM properties. I'll add compiler and runtime regression coverage for literal, static-expression, and dynamic-children case.
yumemi-thomas commentedon Sep 1, 2026
@nickshiro Thanks for tracing it. That explanation matches what I observed.
Before implementing the change, I think it would be useful to get
decision from Ryan on the intended semantics. Should a static JSX
value="2"follow rawHTML attribute behavior, or should all forms of
<select value>represent thelive DOM property consistently?
This may also affect the precedence of
<option selected>, SSR/hydrationbehavior from #3013, and form reset semantics.
@ryansolid could you confirm the intended behavior?
nickshiro commentedon Sep 1, 2026
@yumemi-thomas I'll definitely start with the implementation, because it's not obvious, but the most important thing about changes like this is that the new tests cover these cases. If our hypothesis turns out to be incorrect, we can easily substitute another implementation for the tests.
nickshiro commentedon Sep 1, 2026
I implemented the patch and looked into the edge cases, and this does seem to be a semantic decision rather than only a compiler bug.
The current patch would compile static forms such as:
through the same live DOM property assignment already used for non-static values. That has a few observable consequences:
SSR already strips and resolves it into an option’s selected state for #3013. However, when an option explicitly has selected, SSR preserves that as the default. With this patch, hydration would then apply the live value property and could change the visible selection. A later form reset would return to the explicitly selected option. So the my patch is consistent if the intended rule is that every JSX represents the live DOM property. If static JSX should preserve raw HTML attribute semantics instead, this would be too broad.
Nerivec commentedon Sep 1, 2026
Another similar-ish case I've encountered:
Once loaded, that example will have the select picking "1", instead of proper "2" (both visually, and in the element's
.valueprop).Workaround: adding
selected={val === props.val}on<option>appears to do the trick...Behavior seems a bit "off"? You'd expect the
<Loading>being around the select to handle this properly.ryansolid commentedon Sep 1, 2026
Ruling: unify. On
<select>, JSXvaluemeans selection state everywhere — the static form should behave exactly like the reactive one.For context on why this is compile-output-only: the other cells of the matrix already committed to these semantics. SSR resolves a select's
valueintoselectedon the matching option(s) at flush time (#3013, both static and dynamic), and the dynamic client path assigns the property after children exist (#1754). The static client render is the last cell out of step — the compiler inlines avalueattribute HTML gives no meaning on<select>, and nothing ever assigns the property.So the fix is what @nickshiro traced: stop inlining static
valueon<select>into the template and emit the same post-children property assignment the dynamic path uses. No server changes, no new runtime machinery.Scope notes for the implementation:
@solidjs/babel-plugin-jsxand the native@solidjs/compiler(there are compiled-parity tests — keep them green).<textarea value="…">is the same class of bug (no meaningfulvalueattribute in HTML) and should get the same treatment: property assignment on the client; on the server it's element-local — emit the value as escaped text content, no flush pass needed. Include it here or file it separately, your call.<input value>stays as-is. Itsvalueattribute has real default-value semantics in HTML, so inlining is correct there; this ruling only covers elements where the attribute is dead.multiplewith a static array/string, since Dynamic <select multiple={...}> loses earlier statically selected options during client render #3179 is in the same neighborhood.Go ahead, and thanks for tracing it.
nickshiro commentedon Sep 1, 2026
@ryansolid, Thanks for the feedback; I'm already working working on the PR.
fix(web): assign static select values through DOM property (#3167)