Skip to content

Popover (experiment) ​

EXPERIMENT

Ships as the orio-ui/experiments subpath, not as an auto-imported component. Names, props, and behaviour can change in any release, including a patch. Use <orio-popover> when you need stability.

Popover built on the native HTML Popover API — no teleport, no measured positioning, light-dismiss and Esc handled by the browser.

Live Demo ​

Two instances, so you can check that each panel anchors to its own trigger:

Native popover content.

Click outside or press Esc to dismiss.

Anchored to its own trigger.

Unslotted, to show the defaults — <orio-button> trigger, surfaced panel, fade-and-slide on open:

Test popover

flip="initial" keeps whichever side it opened on, even once scrolling pushes it out of view:

Test popover

Usage ​

Experiments are never auto-imported. Import from the subpath:

vue
<script setup lang="ts">
import { Popover } from "orio-ui/experiments";
</script>

<template>
  <Popover>
    <template #trigger="triggerProps">
      <orio-button v-bind="triggerProps">Open</orio-button>
    </template>

    <template #body="bodyProps">
      <div v-bind="bodyProps">
        <p>Your content here</p>
      </div>
    </template>
  </Popover>
</template>

Both slot bags must be spread onto a real element — the popovertarget / id pairing is what wires the trigger to the panel.

The demos on this page use <orio-x-popover>, the docs-only global alias for the same component.

Props ​

PropTypeDefaultDescription
idstringuseId()Id shared by trigger and panel; keep it unique
positionPopoverPosition'bottom'Placement, as a CSS position-area value
gapnumber0.25Distance between trigger and panel, in rem
flipPopoverFlip'auto'How hard the panel works to stay in the viewport

Position Values ​

A single keyword spans the cross axis, so bottom centers the panel below the trigger:

  • Centered: 'top', 'bottom', 'left', 'right'
  • Edge-aligned: 'bottom span-right' (panel's left edge meets the trigger's), 'bottom span-left', 'top span-right', 'top span-left', 'left span-top', 'left span-bottom', 'right span-top', 'right span-bottom'
  • Corner tiles: 'top left', 'top right', 'bottom left', 'bottom right'

The union is deliberately closed rather than csstype's Property.PositionArea — that type accepts any string, and this value is interpolated into a <style> tag.

Slots ​

SlotPropsDescription
trigger{ popovertarget }Trigger element
body{ id, class, popover }Popover panel

Defaults ​

Unslotted, the component already renders something usable:

  • Trigger — <orio-button>, so it matches the design system out of the box.
  • Surface — panel background, border, radius, padding, and shadow from the theme tokens.
  • Motion — fade and slide away from the trigger on open, via @starting-style and transition-behavior: allow-discrete. Respects prefers-reduced-motion.

All of it is emitted inside :where(), which has zero specificity — a single class of your own overrides any of it, no !important needed:

vue
<template #body="bodyProps">
  <div v-bind="bodyProps" class="my-panel">…</div>
</template>

<style scoped>
.my-panel {
  background: var(--color-accent-soft);
  padding: 1.5rem;
  --popover-enter-distance: 0; /* kill the slide, keep the fade */
}
</style>

Edge flipping ​

Unless flip is 'off', the panel emits position-try-fallbacks, so the browser retries the placement when it would overflow the viewport — block axis first for top/bottom, inline axis first for left/right, then both.

Flip values ​

ValueFlips on openFlips while open
'auto' (default)yesyes — re-resolved on scroll idle and resize
'initial'yesno — the side is fixed once the panel opens
'off'nono — always renders on the requested side

'initial' is the plain-CSS behaviour: the browser emits position-try-fallbacks and resolves them at layout time. 'auto' adds the silent reopen described below.

Why 'auto' needs a trick ​

The browser picks a fallback only when the panel is laid out — that is, when it opens. While it stays open the panel tracks its trigger, but the chosen side is sticky: a panel that opened below its trigger and is then scrolled to the viewport edge stays below, overflowing. Measured in Chrome 151, forcing a style invalidation does not help; rewriting position-area and toggling position-try-fallbacks both leave the resolved side untouched. Only a fresh layout at show time re-resolves it.

So flip="auto" closes and reopens the panel to trigger that fresh layout. The swap is invisible:

  • hide and show happen in the same task, so no frame is ever painted with the panel closed — measured at zero hidden frames;
  • transitions are suppressed for the swap, so the open animation does not replay;
  • the native beforetoggle/toggle pair is swallowed, so listeners on the panel see no spurious close/open;
  • focus and the panel's own scroll offsets are restored, since hiding a popover returns focus to the invoker and the display change would reset scrollTop.

It runs on scroll idle (100 ms after the last scroll event) and on resize, and only when the panel actually overflows the viewport — one relayout per gesture, not one per frame. Drop to flip="initial" if you would rather the panel keep the side it opened on.

Compared to <orio-popover> ​

<orio-popover><orio-x-popover>
Placementmeasured in JS, auto-flipCSS anchor positioning, position-try-fallbacks
Reposition on scrollcapture-phase listenerscompositor tracks the anchor, no JS
Re-flip on scrollyes, re-measured every eventyes, via a silent reopen on scroll idle
Stackingteleport + z-index: 999999native top layer
Light dismisshand-rolled click-outsidebrowser
Esc to closenobrowser
Open stateinternal ref + toggle slot propDOM (popovertarget)

Not implemented ​

  • position-visibility — the panel stays put when its trigger scrolls out of view, instead of hiding with it
  • Focus management (popover="auto" is not a dialog, so focus is not trapped)
  • aria-expanded / aria-haspopup on the trigger bag
  • Browser support check for CSS anchor positioning and @starting-style — where unsupported, the panel falls back to the native centered position and appears without animation