Polyfill Demo Popover Attribute

OddBird’s Popover Attribute Polyfill – built in collaboration with Keith Cirkel and used in production by GitHub – lets developers preview the upcoming mechanism for displaying popover content on top of other page content, drawing the user’s attention to specific important information or actions that need to be taken.

This polyfills the HTML popover attribute and showPopover, hidePopover, and togglePopover methods on HTMLElement, as well as the popovertarget and popovertargetaction attributes on <button> elements.

See which browsers have native Popover API support.

Default Popover
Manual Popover
Empty State Popover
Dialog Popover
Popover with Explicit Action. This popover has an autofocus link.
Single Action Popover. This popover has an autofocus link.
Shadow Invoked Popover
Menu with Shadowed Popover:
Popover with Layered Styles (should be red if browser has @layer support)
Invalid Popover ("invalid")
Invalid Popover ("hint")
Test Popover 1 (auto)
Test Popover 2 (auto)

Default Popover

Popovers take a state of “auto” or “manual”. If no state is provided, the popover takes on the behavior of its default state, which is “auto”. Auto popovers can be “light dismissed” by selecting anywhere on the page, clicking the popover control button, or opening another popover on the page.

<button popovertarget="defaultPopover" aria-expanded="false">
  Toggle Default Popover
</button>

<div id="defaultPopover" popover>Default Popover</div>

Manual Popover

Manual popovers must be dismissed by toggling the control button or clicking another control that is explicity set to hide the popover.

<button popovertarget="manualPopover">
  Toggle Manual Popover
</button>

<div id="manualPopover" popover="manual">Manual Popover</div>

Dialog Popover

<dialog> elements can also be used as popovers.

<button popovertarget="dialogPopover">
  Toggle Dialog Popover
</button>

<dialog id="dialogPopover" popover>Dialog Popover</dialog>

Popover with Empty State

A popover with an empty state behaves as if it were set to “auto”.

<button popovertarget="emptyStatePopover">Toggle Empty State Popover</button>

<div id="emptyStatePopover" popover="">Empty State Popover</div>

Popover Controls with Explicit Actions

Popover button controls can be set to “show” or “hide” with the popovertargetaction attribute. This popover also has a link with an autofocus attribute, which receives focus when the popover is opened.

<button popovertargetaction="show" popovertarget="showHidePopover">
  Show Popover with Explicit Action
</button>
<button popovertargetaction="hide" popovertarget="showHidePopover">
  Hide Popover with Explicit Action
</button>

<div id="showHidePopover" popover="manual">
  Popover with Explicit Action. This popover has an <a href="#" autofocus>autofocus link.</a>
</div>

Popover Control with Only the “Show” Action

Since the value of the popovertargetaction attribute is set to “show”, clicking the button will only open the popover. The popover can be closed by selecting elsewhere on the page, or opening another popover. This popover also has a link with an autofocus attribute, which receives focus when the popover is opened.

<button popovertargetaction="show" popovertarget="singleActionShowPopover">
  Show Single Action Popover
</button>

<div id="singleActionShowPopover" popover>
  Single Action Popover. This popover has an <a href="#" autofocus>autofocus link.</a>
</div>

Popover Control with Invalid Target

Since no popover matches the popovertarget of “invalidTargetPopover”, no popover will open.

<button popovertarget="invalidTargetPopover">
  Toggle Nothing
</button>

Targeting Elements in a Different Tree Scope

This button created in the light DOM has its popovertarget, which is in the shadow DOM, set using JavaScript.

Note that while this use-case is supported by the polyfill, native browser support has lagged behind.

<!-- Light DOM -->
<button id="crossTreeToggle">
  Toggle Cross Tree Popover
</button>

<div id="crossTreeHost">
  <!-- Shadow DOM -->
  <div id="crossTreePopover" popover>Shadowed Popover</div>
</div>

// JavaScript
const crossTreeHost = document.getElementById('crossTreeHost');
const crossTreeShadowRoot = crossTreeHost.attachShadow({ mode: 'open' });
crossTreeShadowRoot.innerHTML = '<div id="crossTreePopover" popover>Shadowed Popover</div>';
document.getElementById('crossTreeToggle').popoverTargetElement = crossTreeShadowRoot.getElementById('crossTreePopover');

Nested Popovers

Popovers can be nested within other popovers.

<button popovertargetaction="show" popovertarget="menuPopover">
  Show Menu
</button>

<div id="menuPopover" popover>
  <ul>
    <li><button>Item 1</button></li>
    <li><button>Item 2</button></li>
    <li>
      <button popovertarget="nestedPopover" popovertargetaction="show">
        Show Sub Menu
      </button>
    </li>
  </ul>
  <div id="nestedPopover" popover style="left: 200px">
    <ul>
      <li><button>Submenu Item 1</button></li>
      <li><button>Submenu Item 2</button></li>
      <li><button>Submenu Item 3</button></li>
    </ul>
  </div>
</div>

Opening a Popover with an Element Created in the Shadow Root

Elements created in the shadow DOM can trigger a popover.

<!-- Light DOM -->
<button popovertarget="shadowInvokedPopover">
  <span id="shadowInvokedHost">
    <!-- Shadow DOM -->
    <span>Toggle Shadow Invoked Popover</span>
  </span>
</button>

<div id="shadowInvokedPopover" popover>
  Shadow Invoked Popover
</div>

// JavaScript
const shadowInvokedHost = document.getElementById('shadowInvokedHost');
const shadowRoot = shadowInvokedHost.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `<span>Toggle Shadow Invoked Popover</span>`;

Nested Popovers in Shadow DOM

Popovers can be nested within other popovers in the shadow DOM.

<!-- Light DOM -->
<button popovertarget="shadowNestedPopover">
  Toggle Menu with Shadowed Popover
</button>

<div id="shadowNestedPopover" popover>
  Menu with Shadowed Popover:
  <div id="menuHost">
    <!-- Shadow DOM -->
    <button popovertarget="shadowedMenuInner">
      Toggle Inner Shadowed Popover
    </button>
    <div id="shadowedMenuInner" popover>
      Inner Shadowed Popover
    </div>
  </div>
</div>

// JavaScript
const menuHost = document.getElementById('menuHost');
const menuShadowRoot = menuHost.attachShadow({ mode: 'open' });
menuShadowRoot.innerHTML = `
  <button popovertarget="shadowedMenuInner">
    Toggle Inner Shadowed Popover
  </button>
  <div id="shadowedMenuInner" popover>
    Inner Shadowed Popover
  </div>`;

Layers Support

When supported, the polyfill creates a cascade layer named popover-polyfill. If author styles are not in layers then this should have no impact. If layers are used, authors will need to ensure the polyfill layer is declared first (e.g. @layer popover-polyfill, other, layers;).

<button popovertarget="layeredStylesPopover">
  Toggle Popover with Layered Styles
</button>

<div id="layeredStylesPopover" data-popover="layered-styles" popover>
  Popover with Layered Styles
  (should be red if browser has <code>@layer</code> support)
</div>

/* CSS */
@layer popover-polyfill;

@layer consumer {
  [data-popover~='layered-styles'] {
    background-color: #d00d1e;
  }
}

Popovers and Controls Created Fully in the Shadow Root

Here, both the popover control button and the popover are created in the Shadow DOM.

<!-- Shadow DOM -->
<button popovertarget="shadowedPopover">
  Toggle Shadowed Popover
</button>
<button popovertarget="shadowedNestedPopover">
  Toggle Shadowed Nested Popover
</button>

<div id="shadowedPopover" popover>Shadowed Popover</div>
<div>
  <div id="shadowedNestedPopover" popover>Shadowed Nested Popover</div>
</div>

Shadow Root Popover with Slotted Contents

Here, both the popover control button and the popover are created in the Shadow DOM, but an element is slotted in the popover.

I’m a slotted element
<!-- Light DOM -->
<div id="shadowHostWithSlot">
  <div><span>I’m a slotted element</span></div>
</div>

<!-- Shadow DOM -->
<button popovertarget="shadowedPopoverWithSlot">
  Toggle Shadowed Popover
</button>

<div id="shadowedPopoverWithSlot" popover>
  <slot></slot>
</div>