x-tooltip is stx's own attribute — the signals runtime implements the tooltip and lists it as handled. But putting it on a component silently converts it into a signal binding over an undefined identifier, which kills hydration for the whole page.
Reproduction
<StxLink to="/settings" class="icon-btn" x-tooltip="Settings" aria-label="Settings">
<Icon name="lucide:settings" size="17" />
</StxLink>Rendered HTML:
<a data-stx-parent-bindings="tooltip" :tooltip="Settings" href="/settings" …>Browser console:
[stx] hydration invariant failed after DOMContentLoaded: 1 expression(s) never evaluated — Settings → Settings is not defined.The x-tooltip attribute the runtime looks for is gone, so the tooltip never works, and Settings is evaluated as a bare identifier.
Cause — two lists that disagree
packages/stx/src/component-renderer.ts:154-159 rewrites any x-* attribute on a component to :* unless the name is in ALPINE_PASSTHROUGH:
const attrName = (rawAttrName.startsWith('x-')
&& !ALPINE_PASSTHROUGH.has(rawAttrName)
&& !rawAttrName.startsWith('x-bind:')
&& !rawAttrName.startsWith('x-on:'))
? `:${rawAttrName.slice(2)}`
: rawAttrNameALPINE_PASSTHROUGH (component-renderer.ts:111-115) has 16 entries. The signals runtime keeps its own list of attributes it consumes, X_HANDLED (signals.ts:2295):
var X_HANDLED = {'x-text':1,'x-html':1,'x-model':1,'x-show':1,'x-if':1,'x-for':1,'x-cloak':1,
'x-ref':1,'x-data':1,'x-bind':1,'x-class':1,'x-style':1,
'x-tooltip':1,'x-tooltip-position':1};x-tooltip and x-tooltip-position are in X_HANDLED and not in ALPINE_PASSTHROUGH. They are the only two in that gap, and they are the two the runtime added most recently (#1673).
Why it is worse than a dead tooltip
The failure is not "the tooltip doesn't show". :tooltip="Settings" becomes a real binding, so the page reports a hydration invariant failure — the class of bug where content silently fails to materialise after an SPA swap. On a page whose value is behind hydration, one mistyped tooltip takes the page down with a console message that names the tooltip text, not the tooltip.
It is also unguessable from the authoring side: the same attribute on a plain <button> works perfectly. Only components mangle it, and every stx builtin is a component — <Icon x-tooltip="…"> fails the same way.
Suggested fix
Derive one list from the other rather than maintaining two. Minimally, add 'x-tooltip', 'x-tooltip-position' to ALPINE_PASSTHROUGH; better, export the runtime's handled-attribute set and have component-renderer consult it, so the next runtime-handled x- attribute cannot reopen this.
A second guard worth having independently: when the rewrite produces a : binding whose expression is a bare identifier that resolves to nothing, that is almost always this mistake rather than intent.
Environment
Reproduced on the fork at f539929dfe (rebuilt dist), driving a real app (loghq) headlessly over CDP. Worked around app-side by hosting x-tooltip on a plain <span> wrapper, which is the only safe host today.