<MeekoPopover>
Accessible popover anchored to its trigger, using the native popover API and CSS anchor positioning, for menus, lightweight dialogs and tooltips.
Deprecated since 1.8; removal in 2.0.
<MeekoPopover>still works in 1.x and emitsMEEK121. Use<UIPopover>from@meekojs/ui0.2 with its module enabled. It preserves props, snippets anddata-meeko-*markup. See Migration to 1.8.
<MeekoPopover> renders a trigger <button> and content in the browser's top layer (the popover attribute), positioned beside the trigger using CSS anchor positioning. It handles what is hard to implement correctly by hand: aria-expanded, aria-controls and aria-haspopup, Esc to close and restore focus, menu keyboard patterns, tooltips that do not steal focus, and positioning that flips when space is insufficient. In browsers without anchor positioning, a JavaScript calculation positions it and keeps it inside the window.
It has no visual styles: it exposes state through data-* and ARIA attributes. Do not import it; the compiler recognizes it and imports its module only in components that use it.
Usage#
<!-- app/components/RowActions.meeko -->
<script lang="ts">
const emit = defineEmits<{ rename: []; remove: [] }>()
</script>
<template>
<MeekoPopover
placement="bottom-end"
bind:offset="4"
role="menu"
>
<snippet name="trigger">Actions ▾</snippet>
<button
role="menuitem"
on:click="() => emit('rename')"
>
Rename
</button>
<button
role="menuitem"
on:click="() => emit('remove')"
>
Delete
</button>
</MeekoPopover>
</template>With role="menu", arrow keys move focus among menuitem elements, letters search for an item, and selecting one closes the menu and restores focus to the trigger.
Type#
<MeekoPopover placement="bottom-start" bind:offset="8" model="open" role="menu" on:toggle="(event) => log(event.open)">
<snippet name="trigger">Opciones ▾</snippet>
<button role="menuitem">Renombrar</button>
</MeekoPopover>interface MeekoPopoverProps extends PopoverProps {}interface PopoverSnippets {
default?: []
trigger?: []
anchor?: [trigger: PopoverTrigger]
}interface PopoverTrigger {
readonly id: string
readonly popovertarget: string | undefined
readonly 'aria-controls': string | undefined
readonly 'aria-expanded': 'true' | 'false' | undefined
readonly 'aria-haspopup': 'menu' | 'dialog' | undefined
readonly 'aria-describedby': string | undefined
}interface PopoverExposed {
show(): void
hide(): void
toggle(): void
}Props#
| Attribute | Type | Default | Description |
|---|---|---|---|
mode |
"auto" | "manual" | "hint" |
"auto" |
"auto" closes on outside clicks and Esc; "manual", only through the trigger, Esc or the model; "hint" is a tooltip opened by the pointer or focus. |
placement |
top, bottom, left or right, with -start or -end |
"bottom-start" |
Side of the trigger and alignment with its edge. |
bind:offset |
number |
0 |
Distance from the trigger in pixels. |
role |
"menu" | "dialog" | "tooltip" |
"tooltip" with mode="hint" |
ARIA pattern for the content. |
label |
string |
none | Content aria-label; without it, a menu or dialog is named by the trigger. |
class, triggerClass |
string |
none | Classes for the content and trigger <button>. |
triggerId |
string |
one from useId |
Trigger id; with a custom trigger, the id it carries. |
matchWidth |
boolean |
false |
Content is at least as wide as the trigger. |
bind:delay, bind:closeDelay |
number |
300, 150 |
With mode="hint", milliseconds before opening and closing with the pointer. |
model |
Signal<boolean> |
none | Open state signal: assigning it opens or closes the popover, and user interaction updates it on opening or closing. |
on:toggle |
(event: { open: boolean }) => void |
none | Emitted on every open and close. |
Snippets: trigger (button content), unwrapped content (default) and anchor, which replaces the button with a custom trigger and receives its attributes in a PopoverTrigger. A ref: receives show(), hide() and toggle().
Result#
- A
<button>withpopovertarget,aria-expanded,aria-controlsandaria-haspopup(depending onrole), containing thetriggersnippet. Withpopovertarget, the button opens and closes the content even before hydration. - Content inside an element with
popoveranddata-meeko-popover, hidden from the server HTML onward. When open, it carriesdata-open; with JavaScript positioning,data-meeko-popover-fallbackanddata-sidewith the selected side.
Examples#
A tooltip#
<label for="code">Code</label>
<input id="code" />
<MeekoPopover mode="hint">
<snippet name="trigger">?</snippet>
The code is on the back of the card.
</MeekoPopover>It opens on pointer hover (after delay) or when focusing the ↑, closes with Esc or on blur and never takes focus. The trigger has aria-describedby referencing the content, which has role="tooltip".
Opening from state#
<script lang="ts">
const open = signal(false)
</script>
<template>
<MeekoPopover
model="open"
role="dialog"
label="Filters"
mode="manual"
on:toggle="(event) => console.log(event.open)"
>
<snippet name="trigger">Filters</snippet>
<form>
<label>
<input type="checkbox" />
Active only
</label>
</form>
<button
type="button"
on:click="() => open.value = false"
>
Apply
</button>
</MeekoPopover>
</template>With role="dialog", focus enters the first control on opening.
A custom trigger#
<MeekoPopover role="menu">
<snippet
name="anchor"
params="trigger"
>
<button
type="button"
class="avatar"
bind="trigger"
>
<img
src="/me.png"
alt="My account"
/>
</button>
</snippet>
<a
role="menuitem"
href="/perfil"
>
Profile
</a>
<a
role="menuitem"
href="/salir"
>
Sign out
</a>
</MeekoPopover>bind="trigger" sets id, popovertarget, aria-controls, aria-expanded and aria-haspopup on the button.
Styles#
[data-meeko-popover] {
border: 1px solid #ddd;
border-radius: 0.5rem;
background: Canvas;
padding: 0.5rem;
}
[data-meeko-popover] [role='menuitem']:focus-visible {
outline: 2px solid Highlight;
}Meeko's anchoring rules are in the meeko cascade layer, so any application rule takes precedence.
Common mistakes#
A placement that does not exist#
<MeekoPopover placement="bottom-left">
<snippet name="trigger">Open</snippet>
…
</MeekoPopover>meeko check reports TS2322: Type '"bottom-left"' is not assignable to type 'PopoverPlacement | undefined'. Use -start or -end:
<MeekoPopover placement="bottom-start">
<snippet name="trigger">Open</snippet>
…
</MeekoPopover>An unsupported role or mode#
<MeekoPopover
role="listbox"
mode="click"
>
<snippet name="trigger">A</snippet>
…
</MeekoPopover>meeko check reports TS2322: Type '"listbox"' is not assignable to type 'PopoverRole | undefined' and TS2322: Type '"click"' is not assignable to type 'PopoverMode | undefined'. For a list of options, use <MeekoSelect>.
<MeekoPopover
role="menu"
mode="auto"
>
<snippet name="trigger">A</snippet>
…
</MeekoPopover>A model that is not boolean#
<script lang="ts">
const open = signal('no')
</script>
<template>
<MeekoPopover model="open">
<snippet name="trigger">Open</snippet>
…
</MeekoPopover>
</template>meeko check reports TS2322: Type 'Signal<string>' is not assignable to type 'Signal<boolean>'.
<script lang="ts">
const open = signal(false)
</script>
<template>
<MeekoPopover model="open">
<snippet name="trigger">Open</snippet>
…
</MeekoPopover>
</template>Menu items without role="menuitem"#
<MeekoPopover role="menu">
<snippet name="trigger">Actions</snippet>
<button>Delete</button>
</MeekoPopover>There is no error, but Meeko does not change the content's role: without role="menuitem", arrow keys do not find the item and screen readers do not announce a menu.
<MeekoPopover role="menu">
<snippet name="trigger">Actions</snippet>
<button role="menuitem">Delete</button>
</MeekoPopover>Notes#
- SSR and hydration. HTML includes the trigger and content hidden by its
popoverattribute, without flicker. IDs linking the trigger and content come fromuseId: they are the same during hydration. - CSP. Nothing goes in
styleattributes: anchoring uses a constructed stylesheet (adoptedStyleSheets), and each instance passes its values through CSSOM. A CSP without'unsafe-inline'is sufficient. - Size. The
@meekojs/meeko/builtins/popovermodule only loads in components using the tag. Do not import it manually: it is a contract between the compiler and its runtime of the same version. - Keyboard.
Esccloses and restores focus to the trigger; an outside click closesmode="auto"without moving focus. Withrole="menu":Enter,Spaceor↓opens with focus on the firstmenuitem(↑, on the last), arrow keys move focus with wrapping,HomeandEndgo to the ends,Tabcloses, and items withdisabledoraria-disabled="true"are skipped. - Custom name. A project component called
MeekoPopovertakes precedence over the tag, with warningMEEK106.