Skip to content

fix(web): dynamic() honors xmlns when creating a tag-name element (#3386) - #3436

Merged
ryansolid merged 3 commits into
nextfrom
fix/dynamic-xmlns
Sep 14, 2026
Merged

ryansolid merged 3 commits into
nextfrom
fix/dynamic-xmlns

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Closes #3386 (part 2; part 1 landed in #3396).

Problem

The compiler resolves a tag's namespace from its parent at build time. The dynamic() runtime path creates the element before it has a parent, so it can only infer from the tag name — and a tag that exists in both HTML and SVG (a, script, style, title) was always created as HTML. <svg><Link href=…/></svg> with Link = dynamic(() => "a") produced an HTML anchor inside the SVG tree. @yak/solid carries an ambiguousSvgTags set to route around this.

Fix

The instance says which namespace it means with the attribute compiled JSX already uses for exactly this purpose:

const Link = dynamic(() => "a");
<svg><Link xmlns="http://www.w3.org/2000/svg" href="/x"></Link></svg>

xmlns joins is as an attribute of the element that also decides how the node is created: read once, untracked, at creation (the DOM can't re-namespace a node), then applied through spread() as an ordinary attribute. That keeps a client-rendered element byte-for-byte the same as its own SSR output: the server already serializes xmlns (as compiled SSR does for <a xmlns>), the parser keeps it, and hydration claims the parser-namespaced node — so the server is untouched and hydration was never affected. Without xmlns inference from the tag name still applies.

Deliberately not inferring from the mount parent: there is no parent at creation time, and any deferred scheme (create-then-reparent, a context) would tax every dynamic() instance to fix four tags.

Types

dynamic() returns Component<DynamicComponentProps<T>>, which adds xmlns?: string for tag-name targets only. (The ambiguous tags' IntrinsicElements types are the HTML attribute sets, which don't include xmlns; SVG/MathML attribute sets already do.)

Tests

  • test/dynamic-namespace.spec.tsx: namespaceURI with/without xmlns inside <svg> and <math>; unambiguous tags still inferred; xmlns overrides inference in both directions; <Dynamic> parity; read-once (a later value updates the attribute, never recreates the node).
  • test/server/dynamic-xmlns.spec.tsx: server output equals compiled <a xmlns> output.
  • Parity-harness scenario dynamic-xmlns-svg-link: SSR → hydrate a dynamic() <a xmlns> inside <svg>, adopt-all, stable identity through an update.
  • Also pins ref arrays through a spread into a dynamic() tag — the shape Kobalte's solid2 branch works around with a TODO at rc.3 — as working on next.

All three @solidjs/web suites green (778 / 895 / 181).

)

The compiler resolves a tag's namespace from its parent at build time; the
dynamic() runtime path creates the element before it has a parent, so a tag
that exists in both HTML and SVG (a, script, style, title) was always an HTML
element — `<svg><Link href/></svg>` with `Link = dynamic(() => "a")` produced
an HTML anchor inside the SVG tree (#3386 part 2).

The instance now says which namespace it means with the attribute compiled
JSX already uses for exactly this (`<a xmlns="http://www.w3.org/2000/svg">`).
Like `is`, `xmlns` is an attribute of the element that also decides how the
node is created: read once, untracked, at creation — the DOM can't
re-namespace a node — then applied through spread() as an ordinary attribute,
so a client-rendered element carries the same attribute the server serializes
and the parser keeps. Server: nothing to do; ssrElement already emits it, and
hydration claims the parser-namespaced node. Without xmlns the namespace is
still inferred from the tag.

Types: dynamic() returns Component<DynamicComponentProps<T>>, adding
`xmlns?: string` for tag-name targets only (the ambiguous tags' intrinsic
attribute types are the HTML ones, which lack it).

Tests: namespaceURI with/without xmlns inside <svg> and <math>, inference
still wins for unambiguous tags, xmlns overrides inference both ways, <Dynamic>
parity, read-once (later values update the attribute, never the node); server
output byte-equal to compiled `<a xmlns>`; parity-harness scenario hydrating a
dynamic() <a xmlns> inside <svg>. Also pins ref arrays through spread into a
dynamic() tag (the shape Kobalte's solid2 branch works around at rc.3) as
working on next.

Co-authored-by: Cursor <cursoragent@cursor.com>
@changeset-bot

changeset-bot Bot commented Sep 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 919b2ab

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@solidjs/web Patch
@solidjs/h Patch
@solidjs/babel-plugin Patch
@solidjs/element Patch
@solidjs/html Patch
test-integration Patch
@solidjs/compiler Patch
@solidjs/diagnostics Patch
@solidjs/signals Patch
solid-js Patch
@solidjs/universal Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codspeed

codspeed Bot commented Sep 14, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 160 untouched benchmarks


Comparing fix/dynamic-xmlns (919b2ab) with next (efcaeba)

Open in CodSpeed

CI test-types rejected the compiled-JSX comparison `<a xmlns={SVG} href>`:
`AnchorHTMLAttributes` had no `xmlns` — the compiler has honored `<a xmlns=…>`
on any tag all along (it's how it disambiguates a/script/style/title), so
the attribute belongs on the shared `ElementAttributes`, not only SVG/MathML.
Source of truth is jsx-h.d.ts; jsx.d.ts and @solidjs/h's copy are regenerated.

With every intrinsic's attribute type carrying `xmlns`, dynamic()'s tag-name
components inherit it and the conditional `DynamicComponentProps` /
`DynamicElementProps` types from the previous commit are unnecessary — drop
them; dynamic() returns Component<ComponentProps<T>> as before.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coveralls

coveralls commented Sep 14, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 34901829164

Coverage remained the same at 71.842%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1007
Covered Lines: 772
Line Coverage: 76.66%
Relevant Branches: 790
Covered Branches: 519
Branch Coverage: 65.7%
Branches in Coverage %: Yes
Coverage Strength: 15.09 hits per line

💛 - Coveralls

Narrow the previous commit: instead of accepting xmlns on every element via
ElementAttributes, keep it on SVGAttributes/MathMLAttributes and add it only
to the four tags that exist in both HTML and SVG (a, script, style, title).
Those are the only tags where the attribute changes what gets created, both
in the compiler and in dynamic(). Introduces TitleHTMLAttributes so title has
a home for it. Regenerates the web and h JSX types.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid

Copy link
Copy Markdown
Member Author

Narrowed the xmlns typing in 919b2ab. The previous commit put it on ElementAttributes, which made every HTML tag accept it; it now lives on SVGAttributes/MathMLAttributes (as before) plus a small AmbiguousNamespaceAttributes mixin on the four tags that exist in both HTML and SVG — AnchorHTMLAttributes, ScriptHTMLAttributes, StyleHTMLAttributes, and a new TitleHTMLAttributes (title was previously bare HTMLAttributes). Those are the only tags where the attribute changes what gets created, in both the compiler and dynamic(); <div xmlns> is rejected again.

Verified locally: @solidjs/web test-types clean, @solidjs/h types build, both xmlns specs pass. Changeset wording updated to match.

Claude via Cursor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants