Navigation✨ Premium

Popover Menu Chain

Popover panel dynamically positioned below its trigger — submenus chained on hover with overflow detection and automatic flip at the viewport edge.

JSPopoverMenu

You'll get access to the interactive demo with a free account.

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Navigation has 36 effects, including 4 free. Explore the category →

3 usage examples

Online editor menu bar — File / Edit / View — Popover Menu Chain example 1

① Online editor menu bar — File / Edit / View

WhenIn a code or rich-text editor embedded in a web page, when the user clicks a toolbar entry.
WhygetBoundingClientRect positioning guarantees precise alignment below each button even in fluid layouts — the automatic flip prevents overflow on narrow viewports.
Settingsbackground: #1e1b4b (dark IDE tone), opacity transition 0.15 s for responsiveness, submenus nested up to 2 levels.
User account menu — SaaS dashboard with nested preferences — Popover Menu Chain example 2

② User account menu — SaaS dashboard with nested preferences

WhenThe user clicks their avatar in a SaaS app header — the panel opens Profile, Settings (with a Theme submenu), and Sign out.
WhyChained submenus group second-level preferences without cluttering the main menu — only one submenu stays open at a time.
SettingsTrigger = 36×36 px avatar, panel min-width 180 px, border rgba(white,0.15) on #111827 background.
Design canvas context menu — Formatting and export options — Popover Menu Chain example 3

③ Design canvas context menu — Formatting and export options

WhenThe user clicks the actions button on a selected element in a Figma-like canvas to access Align, Export (PNG / SVG / PDF), and Duplicate.
WhyOverflow detection and automatic flip are essential on a variable-size canvas — the menu always stays in the viewport without manual handling.
SettingsPanel min-width 170 px, 4 px gap between parent and submenu (adjust the +6 in the JS), submenus for Align → Left/Center/Right and Export → PNG/SVG/PDF.

How it works

On trigger click, the script calls getBoundingClientRect() on both the button and its .popover-scene ancestor to compute relative coordinates. It then sets style.left and style.top directly on the panel — no position: sticky or CSS Anchor Positioning: the calculation runs on every open, ensuring correct alignment even after page scroll.

Items with data-has-sub open their sub-panel on mouseenter: the script first closes all existing .sub-popover.open panels (only one submenu open at a time), then adds the open class to the target via document.getElementById(id).

Overflow detection runs inside a requestAnimationFrame right after opening: if the submenu's getBoundingClientRect().right exceeds window.innerWidth − 10, the position flips to style.left = 'auto' and style.right = 'calc(100% + 6px)' — the submenu folds to the left of its parent.

Closing is handled by a single click listener on document: if the click target belongs neither to the panel nor to the trigger (contains + direct comparison), the closeAll function removes the open class from the panel and all its submenus. CSS transitions (opacity .2s, transform .2s) animate the show/hide.

Accessibility

  • prefers-reduced-motion: no check in the code — opacity .2s, transform .2s transitions remain active. Add @media (prefers-reduced-motion: reduce) { .popover-panel, .sub-popover { transition: none } } if needed.
  • Menu items are native <button> elements — keyboard-focusable and announced by screen readers at the basic level.
  • Missing ARIA: no aria-expanded on the trigger, no aria-haspopup, no role="menu" / role="menuitem" — add these for full WCAG compliance.
  • Escape key to close is not implemented — keyboard users must click outside the panel.
  • Item contrast (#c7d2fe on #1e1b4b): approximate ratio 6.5:1 — WCAG AA compliant for normal-size text.

Browser compatibility

Uses getBoundingClientRect, classList, requestAnimationFrame, and querySelectorAll — universally supported without polyfills. The Popover API pattern is simulated in plain JS, without the native popover attribute.

Chrome 88+✓ Full
Firefox 87+✓ Full
Safari 14+✓ Full
Edge 88+✓ Full
Mobile iOS✓ Touch OK (hover disabled)
Android Chrome✓ Full

Without JS, the panel stays hidden (<code>opacity:0, pointer-events:none</code>) — no fatal errors, the trigger remains visible and focusable.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
<div class="popover-scene" id="popoverScene">
  <button class="popover-trigger-btn" id="popoverTrigger">Menu</button>
  <div class="popover-panel" id="popoverPanel" style="position:absolute;">
    <button class="pop-item" data-has-sub="sub1">Section <span class="arrow">▸</span></button>
    <button class="pop-item">Direct action</button>
    <div class="sub-popover" id="sub1">
      <button class="pop-item">Sub-item 1</button>
      <button class="pop-item">Sub-item 2</button>
    </div>
  </div>
</div>
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
data-has-sub="subId" absent Add this data-attribute to a .pop-item to open the matching #subId on hover. Create as many submenus as needed.
.popover-panel, .sub-popover { background } #1e1b4b Panel and submenu background. Switch to #111827 for a neutral dark or #fff for a light theme.
.popover-panel, .sub-popover { border-color } rgba(99,102,241,.25) Panel border color. Use your brand's primary color to harmonize.
.pop-item:hover { background } rgba(99,102,241,.15) Item hover color. Adapt to your palette (e.g. rgba(255,255,255,.08) for a neutral hover).
transition: opacity .2s, transform .2s .2s Animation duration in the CSS of .popover-panel and .sub-popover. Drop to .12s for faster response or 0s for instant.
gap trigger → panel (JS) 6 px In the JS: t.style.top = s.bottom - c.top + 6 + 'px' — the 6 is the pixel gap between the button's bottom and the panel's top.
gap panel → submenu (CSS) calc(100% + 6px) left property of .sub-popover — controls the horizontal gap between the parent panel's right edge and the submenu.
.arrow (▸) Submenu indicator on items. Replace with an SVG icon or Unicode character (e.g. ›, →, ▶).

FAQ

No. The name 'Popover API pattern' refers to the behavior (dynamically positioned floating panel), not the native HTML attribute. The effect is plain JS using getBoundingClientRect and classList — compatible with Safari 14 and Firefox 87 without polyfills.
Add <button class="pop-item" data-has-sub="mySubId">My item <span class="arrow">▸</span></button> inside #popoverPanel, then <div class="sub-popover" id="mySubId">…</div> right after. The script auto-detects all [data-has-sub] attributes via querySelectorAll.
The script exposes no public API. To open, dispatch a synthetic click: document.getElementById('popoverTrigger').click(). To close, call document.getElementById('popoverPanel').classList.remove('open') then strip .open from all submenus via querySelectorAll('.sub-popover.open').