Headless tooltip store. Hover/focus triggers, open/close delays, Escape dismiss. Pair with @alpinejs/anchor for placement.
pnpm add @ailuracode/alpine-tooltip @ailuracode/alpine-core alpinejsPositioning (Floating UI via Alpine):
pnpm add @alpinejs/anchorimport Alpine from "alpinejs";
import { tooltipPlugin } from "@ailuracode/alpine-tooltip";
Alpine.plugin(tooltipPlugin());
Alpine.start();| Method | Description |
|---|---|
open(id) / close(id) / toggle(id) |
Visibility |
isOpen(id) |
Open state |
register(id, options?) |
Configure delays and lifecycle callbacks |
showOnHover(id) / hideOnHover(id) |
Hover helpers |
showOnFocus(id) / hideOnFocus(id) |
Focus helpers |
handleKeydown(id, event) |
Escape dismiss |
| Option | Default | Description |
|---|---|---|
openDelay |
0 |
ms before opening on hover/focus |
closeDelay |
0 |
ms before closing on mouseleave/blur |
onOpen / onClose |
— | Lifecycle callbacks |
TooltipController owns all mutable tooltip state. The Alpine plugin copies snapshots into $store.tooltip.instances on each change event. Mutating store snapshots directly does not change controller state.
If your application already owns a $store.tooltip — or another toolkit plugin registers on that name — rename the integration surface without touching the controller:
Alpine.plugin(tooltipPlugin({ storeKey: "hints" })); // → $store.hintsThe exposed constant DEFAULT_TOOLTIP_STORE_KEY keeps the rename discoverable from TypeScript.
import {
createTooltipController,
createTooltipStore,
createTooltipStoreFromController,
} from "@ailuracode/alpine-tooltip";
const controller = createTooltipController();
controller.register("help", { openDelay: 150 });
controller.open("help");
const store = createTooltipStore();
// or: createTooltipStoreFromController(controller)| Controller API | Description |
|---|---|
hasInstance(id) |
Whether a tooltip id is registered |
snapshotInstances() |
Shallow readonly copies for adapter sync |
isOpen(id) |
Query open state |
| Removed / changed | Replacement |
|---|---|
controller.instances getter |
snapshotInstances() or hasInstance(id) |
controller.toStore() |
createTooltipStore() or createTooltipStoreFromController(controller) |
<div
x-data
x-init="$store.tooltip.register('help', { openDelay: 150 })"
@keydown.window="$store.tooltip.isOpen('help') && $store.tooltip.handleKeydown('help', $event)"
>
<button
x-ref="helpAnchor"
@mouseenter="$store.tooltip.showOnHover('help')"
@mouseleave="$store.tooltip.hideOnHover('help')"
@focus="$store.tooltip.showOnFocus('help')"
@blur="$store.tooltip.hideOnFocus('help')"
aria-describedby="help-tooltip"
>
Help
</button>
<template x-teleport="body">
<div
id="help-tooltip"
x-show="$store.tooltip.isOpen('help')"
x-anchor.top.fixed.offset.8="$refs.helpAnchor"
role="tooltip"
class="z-50"
>
Tooltip content
</div>
</template>
</div>Delays require client-side timers — initialize on the client via x-init.
- Placement is your responsibility — use
@alpinejs/anchor(x-anchor.*.fixed) for flip, shift, and scroll tracking - Use
<template x-teleport="body">+x-anchor.fixedwhen the floating node sits insideoverflow-hiddenancestors - Wire
@keydown.windowwhile open so Escape works when focus stays on the trigger
MIT