Popover pattern and CSS

A tarted-up whole Pop Tart and one half Pop Tart. It's wearing lipstick. Pop Tarts are not popovers, but they do have pop in the name.

As you may have observed in part 1, default popover styles are pretty bare bones. Browsers display them as a white box with a black order, positioned in the center of the viewport.

A minimally-styled popover in Safari

In this tutorial — part two of the series — we'll look at how to style popovers. You'll learn:

  • where popovers sit in the document stacking context;
  • about the ::backdrop pseudo-element class and :popover-open pseudo-class;
  • how to transition popovers with transition-behavior: allow-discrete and @start-style; and
  • how to position popovers with anchor positioning.

Not all of these features have landed in every user agent. Anchor positioning, in particular, requires a polyfill or fallback as of publication.

Popovers and stacking context

A popover, when open, becomes the top layer in the browser. Popovers also create a new stacking context. Children of the popover are stacked relative to their immediate ancestor instead of to the root element.

The top layer is a newer browser concept introduced with the Fullscreen API and dialog element. As as MDN explains, it is an internal browser concept and cannot be directly manipulated from code.

Here's what this means for your layouts.

  • When open, the popover element is always at the top of the stack.
  • An open popover's ::backdrop pseudo-element is the next layer down.
  • Objects can't be positioned between the popover and its backdrop because they form a single stacking context.
  • Elements within the popover that have a position value of absolute are contained by the popover.

It also means that you can't use the z-index property to adjust the popover's position in the document's stacking order. The popover is always the top-most layer.

Styling popovers with the :popover-open pseudo-class

You can use the attribute selector ([popover]) to select popover elements. But in most cases, you'll want to add styles to opened popovers. That's easy with the :popover-open pseudo-class.

[popover]:popover-open {
    background: pink;
    border-color: transparent;
    border-radius: 1rem;
}

Here's that CSS in action.

Adding the :popover-open pseudo-class increases the selector's specificity. It ensures that popovers are shown only when the user or a script requests it. Using [popover] alone would override the default browser styles. If you added display: grid or display: flex to that rule set, all of the popovers in a document would be visible, regardless of user interaction.

Adding a ::backdrop

The popover pattern also has a ::backdrop pseudo-element. It makes your popover look more like a modal.

A popover in its showing state with a purple, translucent backdrop. It's a white box with a black border and black text

Unlike a true modal, however, other elements in the document do not become inert.

Notice in the preceding demo that when the popover is showing, you can still click on the Popover trigger button to close it. Opening a dialog modal, on the other hand, prevents you from interacting with other elements in the document.

Positioning popovers: in the near future

Because popovers occupy the top layer, positioning works a bit differently than with other elements. Popovers are initially positioned relative to the root element.

With the introduction of anchor positioning, however, we can anchor a popover to the position of another element.

Before we go any further, let me add a caveat: anchor positioning is experimental. Safari 18.3.1 and older do not support it. Neither does any version of Firefox as of this writing (version 138 or prior). Use a polyfill or add fallback styles using @supports not (anchor-name: --projects){}.

Anchor positioning is a blog post unto itself, but let's look at a quick example of how to use it to position an element relative to its trigger. First the markup.

<button popovertarget="moreinfo">Popover trigger</button>
<p popover id="moreinfo">
  This is the popover anchor panel. Its position is anchored to that of its trigger.
</p>

That's pretty straightforward. We have a popover trigger and the popover element. Now the CSS.

[popovertarget=moreinfo] {
    anchor-name: --anc;
}

[id=moreinfo] {
  position-anchor: --anc;
  left: anchor(start);
  top: anchor(end);
  margin: 0;
}

The anchor-name property for the [popovertarget=moreinfo] rule set declares a name for the anchor.

In the [id=moreinfo] rule set, the position-anchor property sets [popovertarget=moreinfo]as the anchoring element and --anc as its name. We can then use the anchor() function to anchor the left edge of the popover to the start of the anchoring element, and the top edge to the end of the anchoring element. Setting margin to 0 overrides the browser's default popover rule, margin: auto;

Popovers and transitions

Remember that showing and hiding a popover toggles between display: none and display: hidden. Because these are discrete values, CSS previously did not allow transitions between them. That's since been resolved by the transition-behavior: allow-discrete property and the @starting-style at rule. Here's an example.

[popover] {
    font-size: 4rem;
    transform: skew(-45deg, -45deg);
    /*
    Can also use
    transition: overlay 1s allow-discrete,
                transform 1s;
    */
    transition: all 1s allow-discrete;
}

[popover]:popover-open {     
    opacity: 1;
    transform: skew(0);

    &::backdrop {
        background: #f60a;
        transition: inherit;
    }
} 

@starting-style {
  [popover]:popover-open {
      opacity: 0;
      transform: skew(-45deg, -45deg);

      &::backdrop {
          background: #f0aa;
          transition: inherit;
      }
    }
}

I've used the all keyword here because I want all of the transitions to happen at once. However, some browsers also support the overlay property, in combination with transition-behavior: allow-discrete.

Transitioning between discrete values requires setting start values for the transition once display: none becomes display: block. That's what the @starting-style at rule does. Once the :popover-open pseudo-class is available, the browser will transition the transform and opacity properties of the popover element. It will also transition the backdrop background color from hot pink to orange.

Browser support is for these features is pretty solid. Firefox, however, doesn't yet transition popovers from their showing state back to their hidden one. Compare the following video to the previous one.

You'll see similar behavior in Safari versions 18.3.1 and earlier.

Key takeaways

HTML popovers simplify a few common front-end patterns. They also introduce a few CSS challenges. Here's what to remember.

  • Popovers are always the top-most layer in the stacking context. Use CSS anchor positioning to create 'help' or 'tool-tip' style components.
  • Popovers create a new stacking context and containing block.
  • Unlike dialog modals, popovers do not make other elements on the page inert.
  • Use transition-behavior: allow-discrete and @starting-style to transition between showing and hidden states.

Happy popping!

Subscribe to the Webinista (Not) Weekly

A mix of tech, business, culture, and a smidge of humble bragging. I send it sporadically, but no more than twice per month.

View old newsletters