Skip to content

Popover ​

A click / hover / focus triggered popup panel that can hold a title, body text and arbitrary custom content. Supports 12-way placement, dual-axis offset, open/close animation, portal, modal mode and nested popovers.

Basic usage ​

Trigger on click

Click to open

Placement ​

placement supports 12 directions: the four bases top / bottom / left / right each with -start / -end cross-axis alignment (bottom-start aligns the panel's left edge with the trigger's left edge, the most common form). When space is insufficient the panel flips along the main axis and keeps the alignment suffix (bottom-start → top-start); after alignment it is still clamped to the viewport.

Four directions

UpDownLeftRight

12-way placement (-start / -end)

bottom-startbottom-endright-starttop-end

Trigger methods ​

trigger controls the trigger method: click (default) / hover / focus / contextmenu / manual, space separated for multiple (e.g. "click hover"). For hover triggering, hover-delay / hover-hide-delay control the open/close debounce (default 150 / 100ms; without debounce hover flickers open/closed); the hover area is the trigger plus the panel (moving across the gap does not close it). manual mode binds no host events at all — visibility is fully controlled by the host open attribute.

Hover trigger

Hover to openClick or hoverRight-click to open

Custom open/close delays

hover-delay=400hover-hide-delay=400

Generic delays (open-delay / close-delay)

open-delay=400close-delay=400

Touchscreen degradation (automatic on coarse pointers) ​

Touch devices (pointer: coarse, e.g. phones and tablets) have no stable hover semantics: a tap synthesizes both hover and click events, which would open-then-immediately-close the panel with the desktop behavior. When trigger includes hover, the hover channel is automatically disabled on coarse pointers and degrades to tap toggling — tap the trigger to open, tap again to close, or tap outside the panel to dismiss (outside dismiss replaces hover-out). Fine-pointer (desktop mouse) behavior is unchanged. Compatible with controlled open and the long-press-delay touch long-press: lifting the finger after a long-press open does not close the panel (800ms gesture-tail guard); the next tap toggles it.

Tap toggling on touch (automatic on touch devices)

Tap me on touch

Disabled ​

disabled disables the whole popover: click / hover / focus / right-click / key triggers are all ignored; the host is desaturated (opacity .6) and aria-disabled is synced. Disabled trigger elements (such as native disabled buttons) do not dispatch mouse events — wrap them in a span before attaching a popover.

Whole popover disabled

Disabled (click)Disabled (hover)

Width ​

width controls the panel width: a number (px), "trigger" (same width as the trigger) or any CSS value (e.g. 50%). width="trigger" fits the "panel as wide as the control" dropdown-select shape.

Width (width)

width="280"width="trigger" (220px)width="50%"same-width (180px)

The boolean attribute same-width is a shortcut for "panel exactly as wide as the trigger" (equivalent to width="trigger", but it also inlines min-width: 0 — strict equality holds even when the trigger is narrower than the panel default min-width of 200px). An explicit width attribute takes precedence; removing same-width hands the width back to CSS.

Offset and collision tuning ​

offset is a dual-axis offset: "main distance" or "main distance, cross offset" (default 8, 0). Collision tuning: collision-padding viewport clamp gap (default 4px); fallback-placements custom fallback sequence (when the requested placement does not fit, each candidate is tried in order); hide-when-detached hides the panel when the anchor is fully outside the viewport.

Dual-axis offset (offset)

offset="12, 20"offset="16"

Collision tuning (collision-padding / fallback-placements / hide-when-detached)

collision-padding=20fallback-placementshide-when-detached

Initial focus and keyboard ​

focus-on-open moves focus into the first focusable element of the panel when opened; initial-focus precisely focuses the element matched by a selector (higher priority, falls back to focus-on-open when unresolvable). trigger-keys toggles open when the listed keys are pressed while the trigger is focused (space separated).

Specified initial focus (initial-focus)

Open (focus lands in the input)

On open, focus goes straight into the input below:

Open with keys (trigger-keys)

Focus then press Enter

Portal ​

append-to moves the panel outside the host container (to body or a selector), avoiding clipping by the host's overflow: hidden / clip; positioning is viewport-based and unaffected by the move. After the panel leaves the shadow, clicks inside the panel still do not trigger outside-click close.

Portal (append-to)

Open portal panel

Arrow and viewport auto adjust ​

By default an arrow pointing at the trigger element's center is shown (the projection of the anchor center onto the panel's pointing edge; when the panel is shifted by viewport-edge avoidance, the arrow still tracks the anchor center); arrow="false" hides the arrow. By default the panel automatically flips along the main axis and avoids the viewport edges when space is insufficient; auto-adjust-overflow="false" disables the auto adjust so the panel keeps the declared placement (it may overflow the viewport).

Arrow visibility and pointing

DefaultNo arrow

Merged arrow (arrow-merge)

bottom-start mergedReference

Auto adjust disabled

Auto adjust off

Custom content ​

Custom content (slot=content)

Open panel
You can place arbitrary custom content via slot="content".

Close button and declarative close ​

closable shows a close button at the panel's top-right (part="close"); clicking it closes and restores focus. Any element inside the content with data-popover="close" becomes a declarative close control — clicking it closes the popover (great for "Done" / "Got it" style action buttons).

Close button and declarative close

closableClose from a panel button
Got it

Color variants ​

color semantic variants: primary / success / warning / danger — a tinted panel background plus a semantic border (arrow included), all derived from tokens (auto-adapted to the dark theme).

Color variants (color)

primarysuccesswarningdanger

Open/close animation ​

The panel plays a fade + scale animation on open and close; transform-origin is direction-aware (expanding from the edge that faces the trigger, hugging the aligned edge for -start/-end). Animations are disabled under prefers-reduced-motion.

Direction-aware animation

Try itRight expand

Live content and auto close ​

By default (without fresh) the content is frozen while closed and the latest value is written when opened; with fresh the closed state keeps syncing content (avoids flicker for controlled content). auto-close auto closes after the given duration (onboarding / guided-tour scenarios).

fresh: content keeps updating while closed

Content → v1Content → v2 (closed)content: -Open to see the content

auto-close: closes after a timeout

Open (closes after 3s)

Nested popovers ​

A floating layer can open a child popover / tooltip from its own content: the child layer anchors correctly and stacks above the parent; closing the parent closes the child as well; Esc closes one layer at a time and restores focus each time.

Nested popovers (pop from a card)

Open main card

Open a child layer from within the parent panel:

Open child card

Virtual triggering ​

In virtual mode there is no real anchor (same as tooltip): the host positions the popover with virtual-x / virtual-y viewport coordinates or a virtual-anchor element selector, ideal for chart / canvas coordinate hints; open controls visibility. In virtual mode neither clicking the trigger nor clicking outside changes the state — the lifecycle is fully host-controlled.

Virtual triggering (canvas cursor tracking)

Move the mouse to see coordinate hints

Virtual triggering (anchor coordinates)

Open at (160, 90)Closeopen: false

Virtual triggering (anchor element)

modal turns the popover into a modal floating layer: full-screen backdrop + focus trap (Tab / Shift+Tab cycle inside the panel, escaped focus is pulled back, only the topmost modal traps) + scroll lock (intercepts wheel / scroll keys while keeping the scrollbar visible) + aria-modal. Clicking the backdrop closes and restores focus.

Modal mode (backdrop + focus lock + scroll lock)

Open modal panel

Focus is trapped inside the panel:

DoneCancel

Accessibility wiring & keyboard ​

The trigger element gets ARIA wiring automatically: aria-haspopup="dialog" + aria-expanded (follows open state) + aria-controls (panel id), so screen readers can perceive the floating layer. trigger-keys defaults to Enter Space — focus the trigger and press Enter / Space to toggle (idempotent with the synthesized click on native buttons, no flicker); override with other keys via the attribute.

Keyboard toggle (default Enter / Space)

Focus, then Enter / Space

trap-focus focus trap ​

trap-focus decouples the focus trap from modal: no backdrop, no scroll lock, but Tab / Shift+Tab cycle inside the panel without escaping — ideal for form popovers (the background stays interactive while focus stays put). modal + trap-focus stack idempotently (the trap is mounted once).

trap-focus (form popover, focus stays inside)

Open form popover
Submit

Close behavior switches ​

close-on-outside / close-on-escape (both default true, preserving current behavior) control "close on outside click" and "close on Esc" respectively; oas-before-close is a cancelable close event (preventDefault() blocks the close). Every close entry point (trigger toggle / outside click / Esc / close button / declarative close / backdrop / auto-close / in-panel select) passes through it first — detail.source labels the origin.

Close switches + cancelable close (oas-before-close)

No outside closeNo Esc closeIntercept-close demo

Size & style variables ​

size tiers: small / medium (default) / large — padding, min-width and font-size scale with the tier (token based). The panel look is customizable via component-level CSS variables: --oas-popover-bg / --oas-popover-border / --oas-popover-shadow / --oas-popover-radius / --oas-popover-padding / --oas-popover-min-width (defaults fall back to global tokens; dark theme follows automatically).

Size tiers (size)

smallmediumlarge

Panel CSS variables (--oas-popover-*)

Variable-styled panel

Structured slots & description ​

slot="header" takes over the whole head region (the title area steps aside); slot="footer" renders a footer action area (separated from the body by a divider); slot="description" renders a supplementary note and wires aria-describedby automatically (screen readers can announce the panel description).

header / footer / description slots

Open structured panel
Member settings Pro
Body content: independent from header / footer / description.

Changes apply immediately on close — no save needed.

CancelOK

Height constraint & in-panel scrolling ​

available-height constrains the panel max-height to the remaining viewport space along the main axis; scrollable enables in-panel scrolling (head / footer fixed, body area scrolls) so long content no longer overflows the viewport. The two are often combined for long lists.

scrollable + available-height (long list scrolls)

Open notifications (scrollable)

Panel height stays within the remaining viewport; the body scrolls internally:

Notice 1Notice 2Notice 3Notice 4Notice 5Notice 6Notice 7Notice 8Notice 9Notice 10

Focus return & empty state ​

final-focus sets where focus goes after closing (selector; or pass an element via the finalFocusEl property, which takes precedence); the default returns to the trigger. hide-empty keeps the panel hidden when it has no content at all (no title / content / slot content) — no blank panels popping up.

final-focus (focus moves to a chosen element after close)

Open (close via ✕ to see focus)

hide-empty (no blank panel)

Clear contentFill contentcontent: (empty)Won't open while empty

Scroll behavior (sticky / close-on-scroll) ​

sticky tiers: partial (default, repositions with the anchor on scroll) / always (once the anchor scrolls out, the panel pins to the viewport edge and stays visible) / off (does not follow scroll). close-on-scroll instead closes on scroll — dropdown-style popovers collapsing on scroll often feels right.

sticky=always (panel pins to the edge after the anchor leaves)

Scroll the area below

↓ Scroll here ↓

close-on-scroll (closes on scroll)

Open, then scroll here

Cursor-anchored context menu & touch long-press ​

With trigger="contextmenu", right-clicking opens the panel at the cursor position (no longer anchored to the trigger center); on touch devices, holding for 500ms opens at the touch point (no right-click on mobile). After opening, scrolling snaps the panel back to the trigger side (a cursor point has no scroll semantics).

Contextmenu capability ships with the main entry: cursor anchoring, touch long-press and the placement/size breakpoint shorthand below live in a separate capability package (subpath @oas-ui/ui/feedback/popover/contextmenu, registered on import); the main entry @oas-ui/ui/feedback/popover already ships it, so no explicit import is needed (demos below use the full entry, which also includes it). If you import the pure-core entry @oas-ui/ui/feedback/popover/core instead, these enhancements are absent — trigger="contextmenu" still opens the panel on right-click (anchored to the trigger element, core behavior), but without cursor anchoring, long-press or breakpoint response (a dev hint points to the explicit import or back to the main entry).

contextmenu cursor anchoring (right-click / touch long-press)

Right-click anywhere around here

The panel appears at the cursor:

Action oneAction two

Auto-close after in-panel select ​

With dismiss-on-select, any click inside the panel (including slot content) counts as a completed selection and closes the panel — no need for data-popover="close" on option panels. The close still passes through oas-before-close (interceptable).

dismiss-on-select (select to close)

Pick one
简体中文English日本語
open: false

Content destroy & lazy mount ​

destroy-on-hide unmounts the panel content presentation on close (slot nodes detached from assignment, attribute text cleared) and re-mounts on reopen — heavy-content popovers (charts / big lists) cost zero rendering while closed, equivalent to lazy mounting. Host light DOM nodes are never removed.

destroy-on-hide (destroy on close, rebuild on reopen)

Open heavy panel

This content is unmounted on close (remounted on reopen):

Content unmounted while closed

Breakpoints & trigger fine-tuning ​

placement / size support breakpoint shorthand (same protocol as space/grid): "bottom md:right" = base value + space-separated breakpoint:value pairs (sm=640 / md=768 / lg=1024 / xl=1280, mobile-first min-width) — below the panel on the bottom, at ≥768px on the right. Breakpoint response is part of the contextmenu capability package (shipped by default via the main entry, see the capability note above; the pure-core core entry falls back to the base value). trigger="mousedown" opens on press (no release needed, one beat faster than click).

placement breakpoint shorthand (resize the window to see it switch)

bottom md:right

trigger=mousedown (opens on press)

mousedownclick (compare)

render-panel pure panel rendering ​

render-panel targets host-assembled scenarios: the popover binds no trigger at all (treated as manual, no trigger ARIA wiring); combined with virtual-x / virtual-y coordinates or append-to, the host fully owns placement and visibility — e.g. custom triggers or canvas-embedded panels.

render-panel (host-assembled trigger)

Host's own button: open pure panelCloseopen: false

Controlled display ​

The open attribute is controlled: an external button can set/remove open to control visibility (clicking outside / pressing Esc still closes it).

Controlled display (open attribute)

OpenCloseopen: falseTrigger element

API ​

oas-popover ​

Attributes ​

AttributeDescriptionTypeDefault
append-toPortal mount target: the panel moves into the target container (body or a CSS selector) when opened and moves back into the host shadow when closed; for panels clipped by host containers (overflow)string—
arrowWhether to show the arrow (default true; arrow="false" hides it, the element and ::part(arrow) are kept)stringtrue
arrow-mergeMerge the arrow with the panel corner (C1): a right triangle merges flush with the panel corner — legs collinear with the panel edges (their strokes continue the panel border band), tip pointing orthogonally toward the anchor; the matching corner radius is zeroed; only takes effect for -start/-end placements, center placements are unaffectedboolean—
auto-adjust-overflowViewport-edge auto flip and avoidance (default true; "false" disables it, keeping the declared placement, which may overflow the viewport)stringtrue
auto-closeAuto close after opening for the given duration in ms, e.g. auto-close="3000"; not set means no auto closestring—
available-heightMax content height constrained by remaining viewport spaceboolean—
closableShow a close button at the panel's top-right (part="close"); clicking it closes and restores focus to the triggerboolean—
close-delayGeneric close delay in ms (default 0; used by non-hover trigger paths, hover paths prefer hover-hide-delay)string—
close-on-escapeClose on Escape (default true)——
close-on-outsideClose on outside click (default true)stringtrue
close-on-scrollClose on scrollboolean—
collision-paddingViewport-edge clamping padding in px (default 4), the gap kept when the panel avoids viewport edgesstring—
colorColor variant: primary / success / warning / danger (tinted panel background + semantic border, derived from tokens including dark variants); unset or invalid keeps the neutral panelstring—
contentBody textstring—
destroy-on-hideDestroy the panel content presentation on close (host light DOM nodes are kept: their slot names are rewritten to detach them and they are hidden; attribute text is cleared); remounted instantly on reopenboolean—
disabledDisable the whole popover: click / hover / focus / contextmenu / trigger-keys triggers are all ignored; the host is desaturated and aria-disabled is syncedboolean—
dismiss-on-selectClicks inside the panel (named slot content / panel internals including the close button) count as a completed selection and close it (interceptable via before-close); clicks on the trigger element do not dismiss (guards against open-then-close)boolean—
fallback-placementsCustom fallback sequence (comma or space separated, e.g. "left, right"): when the requested placement does not fit, each candidate in the sequence is tried for fit; the first fit wins, if none fit the last one is clamped; unset uses the default main-axis flipstring—
final-focusFocus restore target after close (selector)string—
focus-on-openMoves focus into the first focusable element of the panel when openedboolean—
freshKeep updating the content while closed (by default the content is frozen while closed and the latest value is written when opened; with fresh the closed state keeps writing)boolean—
hide-emptyHide the panel when it has no contentboolean—
hide-when-detachedHide the panel when the anchor is fully detached from the viewport (the open state is kept, avoiding a panel floating off-screen)boolean—
hover-delayHover-trigger open debounce in ms (default 150; falls back to open-delay when unset)string—
hover-hide-delayHover-trigger close debounce in ms (default 100; falls back to close-delay when unset)string—
initial-focusFocus the element matched by the selector when opened (host light DOM first, including slot content; falls back to focus-on-open when unresolvable), higher priority than focus-on-openstring—
long-press-delayTouch long-press delay in ms (default 500; contextmenu trigger only, part of the contextmenu capability package — see the capability note above)——
modalModal mode: full-screen backdrop + focus trap (Tab cycles inside the panel) + scroll lock + aria-modal; clicking the backdrop closesboolean—
offsetDual-axis offset: "main distance" or "main distance, cross offset" in px (default 8, 0), e.g. offset="12, 20"——
openControlled display (boolean attribute; shows when present)boolean—
open-delayGeneric open delay in ms (default 0; used by non-hover trigger paths, hover paths prefer hover-delay)string—
placementPopup placement (12 directions: four bases top/bottom/left/right each with -start/-end cross-axis alignment)stringtop
render-panelPure panel rendering mode: no trigger semantics (always treated as manual), positioning falls back to virtual coordinates/anchor; position it yourself via append-toboolean—
same-widthPanel width equals the trigger width (strict equality; explicit width takes precedence)boolean—
scrollableScrollable panel when content overflowsboolean—
sizeSize presetstringmedium
stickySticky against viewport edge: partial (default) / always / off (disables edge-following)stringpartial
titleTitle text (rendered into the visible title region; absorbed from the host on read so no native hover tooltip remains; pass an empty string to clear); use slot="title" for rich contentstring—
trap-focusStandalone focus-trap toggle (decoupled from modal mask; Tab stays inside form popovers)boolean—
triggerTrigger method: click (default) / hover / focus / contextmenu / manual, space separated for multiple (e.g. "click hover"). With hover, touch devices (coarse pointer) automatically degrade to tap toggling: tap to open, tap again or tap outside to close; desktop hover behavior unchangedstringclick
trigger-keysToggle open when the listed keys are pressed while the trigger is focused (space separated, e.g. "Enter Space"; bound to Enter Space by default)stringEnter Space
virtualVirtual trigger mode (same as tooltip; no anchor element)boolean—
virtual-anchorVirtual anchor element selector (used when virtual-x/virtual-y are unset)——
virtual-xVirtual anchor x (viewport coordinate, px)——
virtual-yVirtual anchor y (viewport coordinate, px)——
widthPanel width: a number (px) / "trigger" (same width as the trigger) / any CSS value (e.g. 50%, 240px); unset keeps the defaultstring—

Events ​

EventDescription
oas-before-closeEmitted before a close request (all close entries converge here: trigger/outside/escape/close-btn/declarative/backdrop/auto/dismiss/scroll), cancelable, detail: { source }; preventDefault blocks the close
oas-open-changeopen state changed, detail: { open }

Slots ​

NameDescription
defaultPanel body content
contentPanel content (equivalent to the default slot; named for explicit use)
descriptionRich description slot (wired via aria-describedby)
footerStructured footer slot
headerStructured header slot
titleRich title content slot, overrides the title attribute text when present

CSS Variables ​

CSS VariableDefault
--oas-glass-blurnone
--oas-glass-ringtransparent
--oas-origin-xcenter
--oas-origin-ycenter
--oas-popover-arrow-height12px
--oas-popover-arrow-radius0
--oas-popover-arrow-width12px
--oas-popover-bgvar(--oas-color-bg)
--oas-popover-bordervar(--oas-color-border)
--oas-popover-font-sizevar(--oas-font-size-md)
--oas-popover-min-width200px
--oas-popover-paddingvar(--oas-space-4)
--oas-popover-radiusvar(--oas-radius-md)
--oas-popover-shadowvar(--oas-shadow-md)

Clicking the trigger toggles visibility; clicking outside or pressing Esc closes it; role="dialog". Nested popovers: closing the parent cascades to children; Esc closes one layer at a time and restores focus to the trigger.

Released under the MIT OR Apache-2.0 License.