popover api in lwc

Popover API in LWC: A Modern Way to Create Popovers

The Popover API is an exciting new web platform feature that brings native support for creating popup content without the need for complex JavaScript logic or third-party libraries. This API provides built-in animations, keyboard navigation, and focus management - all with just a few HTML attributes. While currently in limited browser support, it represents the future of how we'll handle tooltips, menus, and other popup content on the web.

The API shines in its simplicity - by adding popover and popovertarget attributes to your elements, you get a smooth, animated popup that follows all the best practices for accessibility and user interaction. For a deep dive into the API's capabilities, check out the MDN documentation.

The Challenge in LWC

While implementing the Popover API in Lightning Web Components (LWC) seems straightforward at first, we quickly run into a challenge. The API requires that we link our trigger element to our popover using an ID:

1<button popovertarget="my-popover">Open Popover</button>
2<div id="my-popover" popover>Popover content</div>

However, the LWC rendering engine will generate dynamically element's ID to ensure uniqueness across components. This means we can't hardcode IDs in our template, as they'll be transformed at runtime... Kind of...

For example, if we write:

1<button id="myid">Open Popover</button>

LWC will generate something like:

1<button id="myid-1234">Open Popover</button>

The Solution

Render the element that has the attribute [popovertarget] only when our component is aware of that [popover] element's dynamic ID

 1<template>
 2  <template lwc:if={popoverId}>
 3    <button
 4      popovertarget={popoverId}>
 5    </button>
 6  </template>
 7  <aside
 8    id="popover"
 9    popover="manual">
10  </aside>
11</template>

Then, on the JS side

 1import { LightningElement, api, track } from 'lwc';
 2
 3export default class myLWC extends LightningElement {
 4 @track $popover;
 5
 6 set popover(v) {
 7  this.$popover = v;
 8  if (v.id) this.renderedCallback();
 9 }
10  get popoverId() {
11    return this.$pop?.id;
12  }
13
14  renderedCallback() {
15    if(this.popoverId === undefined)
16      this.popover = this.template.querySelector('[popover]');
17  }
18}

Anchor positioning

The Popover API provides powerful positioning capabilities through the anchor attribute. This attribute determines how the popover positions itself relative to its trigger element.

The way it works is by setting the same archor name on the triggering and the target element.

1/*triggering element*/
2[popovertarget] {
3  anchor-name: --uniqueName;
4}
5/*target element*/
6[popover] {
7  position-anchor: --uniqueName;
8}

Doing that, both elements are now linked, meaning we can position the target element based on the triggering element position.

 1/*triggering element*/
 2[popovertarget] {
 3  anchor-name: --uniqueName;
 4}
 5/*target element*/
 6[popover] {
 7  position-anchor: --uniqueName;
 8  top: anchor(top);
 9  bottom: anchor(bottom);
10  right: anchor(right);
11  left: anchor(left);
12}

Now to ensure uniqueness for our anchor name in LWC we could simply reuse our popoverId property.

1get anchorName() {
2  return `anchor-name: --${this.popoverId}`;
3}
4get positionAnchor() {
5  return `position-anchor: --${this.popoverId}`;
6}
 1<template>
 2  <template lwc:if={popoverId}>
 3    <button
 4      popovertarget={popoverId}
 5      style={anchorName}>
 6    </button>
 7  </template>
 8  <aside
 9    id="popover"
10    style={positionAnchor}
11    popover="manual">
12  </aside>
13</template>

Adding animations

To animate the entering and exiting state of our popover we'll leverage the new css rule @starting-style and the property transition-behavior: allow-discrete.

You can add to the head of your community these styles:

 1[popover] {
 2  transition:
 3    display .2s allow-discrete,
 4    opacity .2s ease,
 5    scale .2s ease;
 6
 7  &:not(:popover-open) {
 8    opacity: 0;
 9    scale: 0;
10  }
11
12  &:popover-open {
13    opacity: 1;
14    scale: 1;
15  }
16
17  @starting-style {
18    &:popover-open {
19      opacity: 0;
20      scale: 0;
21    }
22  }
23}

Of course you can customize the different behaviours according to your needs or even creating custom properties to allow for more in depths customization.

Creating a Reusable Popover Component

Let's create a dedicated popover component that we can reuse across our application:

 1// c-popover/popover.js
 2import { LightningElement, api } from 'lwc';
 3
 4export default class Popover extends LightningElement {
 5  @track $popover;
 6
 7 set popover(v) {
 8  this.$popover = v;
 9  if (v.id) this.renderedCallback();
10 }
11  get popoverId() {
12    return this.$pop?.id;
13  }
14  get anchorName() {
15    return `anchor-name: --${this.popoverId}`;
16  }
17  get positionAnchor() {
18    return `position-anchor: --${this.popoverId}`;
19  }
20
21  renderedCallback() {
22    if(this.popoverId === undefined)
23      this.popover = this.template.querySelector('[popover]');
24  }
25}
 1<!-- c-popover/popover.html -->
 2<template>
 3  <template lwc:if={popoverId}>
 4    <button
 5      style={anchorName}
 6      popovertarget={popoverId}>
 7      <slot name="thumb"></slot>
 8    </button>
 9  </template>
10  <aside
11    style={positionAnchor}
12    id="popover"
13    popover="manual">
14    <slot></slot>
15  </aside>
16</template>
 1/* c-popover/popover.css */
 2button[popovertarget] {
 3  all: unset;
 4  cursor:pointer;
 5}
 6
 7[popover] {
 8  top: anchor(var(--popoverTop));
 9  bottom: anchor(var(--popoverBottom));
10  right: anchor(var(--popoverRight));
11  left: anchor(var(--popoverLeft));
12  translate: var(--popoverX) var(--popoverY);
13}

Now we can use our popover component like this:

1<c-popover>
2    <h2 slot="thumb">Popover Title</h2>
3    <p>This is the popover content that will be displayed when the button is clicked.</p>
4</c-popover>

So far the popover will show when clicking on the triggering element, but what if we want to show it on hovering it ?

I got you covered. You can add

1/* c-popover/popover.css */
2:host[data-hover] button[popovertarget]:hover + [popover] {
3  opacity: 1;
4  scale: 1;
5}
1<c-popover data-hover>
2    <h2 slot="thumb">Popover Title</h2>
3    <p>This is the popover content that will be displayed when the button is hovered.</p>
4</c-popover>

Here's a demo of how it looks like in our own application:

popover api demo

This implementation gives us a reusable popover component that:

With this foundation, you can build more complex popovers by adding features like:

Remember to check browser compatibility and consider providing a fallback for browsers that don't yet support the Popover API. Despite limited current support, implementing the API now positions your application to take advantage of this native functionality as it becomes more widely available.