x-tooltip is the one attribute that a component neither forwards to its concrete root nor rewrites into a binding. It lands only inside data-stx-props, and the tooltip runtime reads it off the DOM, so putting it on a component deletes the tooltip with no error, no warning and no visible difference until you hover.
Measured on 0.2.532.
Reproduction
import { processComponents } from '@stacksjs/stx/dist/component-renderer.js'
const run = (src: string) => processComponents(src, {
__stx_client_signal_names: [],
__importedComponents: new Map([['button', '<path to>/@stacksjs/components/src/ui/button/Button.stx']]),
}, 'x.stx', { componentsDir: 'resources/components' }, new Set())
await run('<Button x-tooltip="Remove">Remove</Button>')attribute on <Button> | emitted on the concrete root | present in data-stx-props |
|---|---|---|
x-tooltip="Remove" | no | yes |
x-tooltip-position="top" | no | yes |
title="Remove" | yes | yes |
aria-label="Remove" | yes | yes |
data-row="7" | yes | yes |
The two that fail are exactly the two the runtime consumes from the DOM. dist/chunk-g5t10z04.js:
function showTip(el) {
var text = el.getAttribute("x-tooltip");
if (!text) return;
...
}The attribute is never on the element, so showTip returns early and nothing renders.
Why this is a gap rather than the older bug
The previously reported failure was different and is fixed: component-renderer.js rewrites an x-* attribute on a component into a :name signal binding unless the name is in ALPINE_PASSTHROUGH, and x-tooltip used to be absent from that set, so x-tooltip="Settings" became :tooltip="Settings" and the page died with hydration invariant failed ... Settings is not defined.
In 0.2.532 it is in the set:
// dist/runtime-globals.js
export const RUNTIME_HANDLED_X_ATTRS = [..., "x-tooltip", "x-tooltip-position"]
export const COMPONENT_PASSTHROUGH_X_ATTRS = [...RUNTIME_HANDLED_X_ATTRS, ...ALPINE_BRIDGE_X_ATTRS]// dist/component-renderer.js
const attrName = rawAttrName.startsWith("x-") && !ALPINE_PASSTHROUGH.has(rawAttrName) && ... ? `:${rawAttrName.slice(2)}` : rawAttrNameSo it correctly escapes the rewrite. What it does not do is reach the root. Since #2063 forwards DOM, data-* and aria-* attributes to the concrete root, x-tooltip falls between the two paths: too x- to be forwarded as a DOM attribute, too passthrough to become a binding.
The crash was louder and therefore easier. This one costs a tooltip per call site and shows up in review as "the tooltip feels missing".
Impact
It blocks component adoption wherever a tooltip is involved. In loghq's settings page, nine buttons hand-roll :disabled="someBusy()" with no aria-busy; five could adopt <Button> and did, and the remaining four are blocked only by this. title works and is a different tooltip (the browser's, not the app's), which is not a trade worth making for a consistent surface.
Expected
An x-* attribute in COMPONENT_PASSTHROUGH_X_ATTRS should be emitted on the concrete root, the same way title and data-* are. x-cloak, x-ref and x-transition are presumably in the same position, since they are also read off the element by the runtime rather than evaluated as expressions.