Skip to content

Popover

Menus, lightweight dialogs and tooltips anchored to their trigger, with keyboard support and the native top layer.

UIPopover places its content in the browser's top layer with the popover attribute and anchors it to the trigger with CSS anchor positioning, without JavaScript. When it does not fit, it moves to the other side with position-try-fallbacks. A browser without anchoring opens it unanchored: it still works.

With role="menu", the children with role="menuitem" are navigated with the arrow keys, Home, End and the first letter of each action. Esc closes the menu and returns focus to the trigger.

meeko
<script lang="ts">
  const action = signal('')
</script>

<template>
  <UIPopover role="menu" placement="bottom-start" bind:offset="6">
    <snippet name="trigger">Project actions</snippet>
    <UIButton variant="ghost" role="menuitem" on:click="() => (action.value = 'rename')">Rename</UIButton>
    <UIButton variant="ghost" role="menuitem" on:click="() => (action.value = 'duplicate')">Duplicate</UIButton>
    <UIButton variant="ghost" role="menuitem" on:click="() => (action.value = 'archive')">Archive</UIButton>
  </UIPopover>
</template>

Lightweight dialog

With role="dialog" focus moves into the content when it opens. Use it for brief controls that do not need to block the page; for a task that demands attention, use Modal. The model controls the opening from code.

Showing published projects.

meeko
<script lang="ts">
  const opened = signal(false)
</script>

<template>
  <UIPopover role="dialog" label="Filter projects" model="opened">
    <snippet name="trigger">Filter</snippet>
    <UICheckbox label="Published" model="published" />
    <UIButton on:click="() => (opened.value = false)">Apply filters</UIButton>
  </UIPopover>
</template>

Tooltip

With mode="hint" the content is a tooltip: it opens when the pointer hovers, after delay, or when the trigger is focused, and it never receives focus. The trigger references it with aria-describedby. Write supplementary information in it, never the only explanation of an action.

meeko
<UITooltip bind:offset="6">
  <snippet name="trigger">Status: draft</snippet>
  Only your team can see it until you publish it.
</UITooltip>

Your own trigger

The anchor snippet replaces the button that UI renders. It receives the attributes of the trigger to apply them with bind="trigger"; UIPopover finds it by its id and adds the keyboard handling and the anchoring.

meeko
<UIPopover role="menu">
  <snippet name="anchor" params="trigger">
    <button type="button" class="avatar" bind="trigger">
      <img src="/avatar.png" alt="My account">
    </button>
  </snippet>
  <button role="menuitem">Profile</button>
  <button role="menuitem">Sign out</button>
</UIPopover>

API

UIPopover props
Name Type / default Description
model Signal<boolean> false Two-way opening of the content.
mode auto | manual | hint auto auto closes on an outside click and with Esc; manual only from the trigger, Esc or the model; hint is a tooltip.
role menu | dialog | tooltip ARIA pattern of the content. With mode=hint, tooltip by default.
placement top | bottom | left | right, with -start or -end bottom-start Side and alignment relative to the trigger.
offset number 0 Distance in pixels from the trigger.
label string Accessible name of the content. Without it, a menu or a dialog is named after the trigger.
matchWidth boolean false The content is at least as wide as the trigger.
delay / closeDelay number 300 / 150 With mode=hint, milliseconds before opening and closing.
class / triggerClass string Classes of the content and of the trigger button.
triggerId string id of the trigger. By default, one that is stable between SSR and hydration.

It accepts the default, trigger and anchor snippets. The toggle event receives an object with open every time the content opens or closes. With a ref:, the component exposes show(), hide() and toggle(). The UIPopoverProps, PopoverPlacement and PopoverTrigger types are exported from @meekojs/ui/runtime.

Was this page helpful?

Add a comment

It is sent when you choose “Yes” or “No”. Only the page, your answer and the comment are stored: no cookies and no personal data.

0 of 2000 characters