Skip to content

Literal "undefined" in a web component's light DOM is usually Svelte 4's {@html undefined}, not a real UI bug

Symptom. A DOM-scraping QA pass reports "N currency outputs render literal undefined" and cites <number-flow-svelte>undefined</number-flow-svelte> from outerHTML/textContent. Screenshots of the same region show correct values.

Root cause. @number-flow/svelte (0.3.x through 0.4.2) ends its template with the SSR fallback:

{@html BROWSER ? undefined : renderInnerHTML(data, { nonce, elementSuffix: '-svelte' })}

Under Svelte 4, HtmlTag.h(html) does this.e.innerHTML = html (svelte/src/runtime/internal/dom.js), and innerHTML = undefined stringifies to "undefined". Svelte 5 coerces nullish to '', so this is Svelte-4-only. Only client-created instances are affected: SSR-rendered ones hydrate by claiming the server's markup and the {@html} expression never updates (it is the constant undefined), so the artifact appears exactly on components that mount after hydration (a mobile results drawer, a modal, anything behind a toggle). That asymmetry, same component but different light DOM in different places on one page, is the tell.

Why it is invisible. number-flow attaches an open shadow root with no <slot>, so non-slotted light DOM children generate no boxes. Verified on production: the text node's Range.getBoundingClientRect() is 0x0, it is absent from innerText, from the SSR HTML (curl | grep undefined is empty, so SEO is unaffected), and from Accessibility.getFullAXTree (1394 nodes, zero matches), while the element's own accessible name is the real "$3,034". Same class of non-issue for any slot-less custom element.

Trap: Puppeteer's ariaSnapshot() lies here. It printed - text: undefined inside the element. Chrome's own full AX tree does not contain it. When an a11y claim rests on a snapshot helper, confirm with CDP Accessibility.getFullAXTree before calling something user-visible.

Triage recipe for any "literal undefined in the DOM" report:

const n = document.querySelector('my-element');
n.shadowRoot?.querySelectorAll('slot').length;         // 0 => light DOM is dead weight
const t = [...n.childNodes].find(c => c.nodeType === 3);
const r = document.createRange(); r.selectNodeContents(t);
r.getBoundingClientRect();                             // 0x0 => not rendered

Plus curl the URL for SSR exposure and a screenshot for the human-visible truth. Only after all four come back clean is it safe to close as cosmetic.

Fix options, in cost order. (1) Nothing but a code comment where the wrapper lives; the artifact is inert, and upstream still ships it in 0.4.2, so an upgrade will not clear it. (2) patch-package the one-line template (BROWSER ? '' : ...) if you already run a postinstall patch step. (3) Do not hand-roll a replacement for the vendor Svelte wrapper just to clean textContent; you inherit formatToData and the __svelte_* prop-setter plumbing and risk the animation for zero user-facing gain.

Generalisation. In Svelte 4, {@html x} with a nullish x renders the string "undefined" anywhere, not just in web components. Grep libraries for {@html before trusting DOM-text assertions, and never promote a textContent finding to "user-visible" without a rendered-geometry or AX-tree check.

No signals yet