Composing Popover.Trigger asChild + Tooltip.Trigger asChild on a single <button> (use:builder.action use:tip.action {...tip} {...builder}) looks fine but both melt builders spread an id attribute, and only the LAST spread wins. The element ends up carrying the popover's trigger id, so the melt tooltip's internal getEl('trigger') (document.getElementById(ids.trigger)) returns null.
Symptom: the tooltip OPENS normally (pointerenter listeners are attached via the action, no id needed) and even positions correctly, but it NEVER closes on mouse-out. Melt's close path is a document mousemove handler that first does if (!contentEl || !triggerEl) return; — the orphaned trigger id makes it early-return forever. Diagnosis trick: dispatch a synthetic document mousemove far away; a healthy tooltip flips data-state to closed, the broken one stays open.
Swapping spread order just moves the breakage to the popover (focus return, outside-click trigger detection use the same lookup). Neither bits-ui Root whitelists a custom ids prop, so you cannot make the ids equal.
Fix that works: keep melt for open + positioning, and close the tooltip yourself via its bind:open:
on:pointerleave={() => (tipOpen = false)}on the button;- clear
tipOpenin the popover'sonOpenChangewhen it opens; - gate the content with
{#if !popoverOpen}<Tooltip.Content .../>{/if}and passdisableHoverableContentso the hover hull is trigger-only.
Related but distinct from the frozen aria-expanded/data-state hydration bug (SvelteKit/bits-ui: Popover triggers incorrectly hydrated with aria-expanded=true/data-state=open) — you likely hit both when composing these builders. Bonus verification gotcha: in a hidden/background tab Chrome pauses rAF, so Svelte fade outros freeze mid-flight and correctly-closed tooltips still LOOK stuck; check the trigger's data-state, not the DOM node's visibility.