← All writing
Craft · · 6 min

The popover attribute

On the top layer ending an entire genre of bug, the difference between this and a dialog, and the half of the feature that hasn't arrived yet.

HTML Accessibility

Here is a component I have written, in some form, on nearly every project since about 2012.

A button. A panel that appears when you press it. Click outside and it closes. Press Escape and it closes. Focus goes into it when it opens and comes back to the button when it shuts. It appears above everything else on the page. The button gets aria-expanded so assistive technology knows what’s going on.

That’s a menu, a tooltip, a filter panel, a share sheet, a notification, a custom select. Six behaviors, about 180 lines by the time you’ve handled the edge cases, and I have got at least two of them wrong every single time.

menu.html
<button popovertarget="filters">Filters</button>

<div id="filters" popover></div>
HTML

All six. No JavaScript.

The top layer

The one I’d single out, because it ends a category of bug rather than saving typing.

A popover renders in the top layer, which is a separate rendering surface above the entire document. Not “with a very high z-index.” Above, outside the stacking order entirely.

Which means the thing I have debugged more times than almost anything else in my career simply cannot happen. The dropdown that appears behind the header. The tooltip clipped by an ancestor with overflow: hidden seven levels up, which is the same ancestor that ate my sticky positioning in 2017. The panel that works everywhere except inside the carousel because the carousel established a stacking context. The arms race where every component’s z-index is one higher than the last one somebody was annoyed by.

All of that is a consequence of positioned elements living in the document’s stacking order, and a thing in the top layer isn’t in it.

It doesn't have a very high z-index. It's outside the stacking order altogether, which means an entire genre of bug I've been debugging my whole career can't occur.

auto and manual

Two behaviors, and the default is the one you want.

popover or popover="auto" gives you light dismiss: clicking outside closes it, Escape closes it, and opening another auto popover closes this one. That last part is a stack rather than a rule, so a nested popover inside another one stays open, which is what menus need.

popover="manual" opts out of all of that. Nothing closes it except you. This is for things that shouldn’t vanish when somebody clicks elsewhere: a toast, a notification, a persistent panel.

The trigger can be more specific than a toggle, too:

actions.html
<button popovertarget="panel" popovertargetaction="show">Open</button>
<button popovertarget="panel" popovertargetaction="hide">Close</button>
HTML

And there’s a script API when you need one: showPopover(), hidePopover(), togglePopover(), plus beforetoggle and toggle events, and a :popover-open selector for styling the open state.

It is not a modal

The mistake I expect to see most, and I nearly made it in the first two weeks.

A popover is non-modal. The rest of the page stays interactive, focus can leave it, and it doesn’t block anything. That’s correct for a menu or a tooltip, where the whole point is that the page underneath is still live.

A modal dialog is a different thing: the rest of the page should be inert, focus should be trapped, and Escape means cancel rather than dismiss. There’s a separate element for that with its own behavior, and I’ll write that one up properly in a few months because it has its own set of things people get wrong.

The test: if the person can sensibly carry on using the page behind it, popover. If they must deal with this before anything else, dialog.

The positioning half isn’t here yet

The one real gap, and it’s worth knowing before you plan around this.

The attribute puts the panel in the top layer. It does not position it relative to the button that opened it. So a dropdown that should appear directly beneath its trigger, flipping above when there’s no room below, still needs either some CSS you’ve worked out by hand or the same positioning library you were already using.

There’s a companion feature for exactly that, letting an element anchor itself to another and reposition when it would overflow, and it’s shipping in one engine shortly and isn’t broadly available. When both are everywhere, this whole category of component becomes markup. Until then you’ve deleted the state management and kept the math.

The gotchas

Don’t set display on it. The browser’s own styles use display: none to hide a closed popover and remove that when it opens. Setting display: flex or display: block on the element overrides the closed state and you get a popover that’s permanently visible, which looks like the attribute isn’t working at all. Put your layout on a wrapper inside it.

Animating it is fiddly. Transitioning something in and out of display: none has always been the awkward case in CSS, and the pieces that make it work properly, a rule for the starting state and permission to transition a discrete property, are newer than the popover itself and aren’t everywhere yet. A fade is achievable and it’s more work than you’d expect for a fade.

One auto popover at a time. Usually what you want, occasionally surprising if you’d assumed two unrelated panels could be open together. Use manual for those.

An accessibility fix

I keep writing this same observation and here it is again: the platform absorbed a thing that was previously a reason to install something.

What’s different about this one is that it isn’t only saving code. Every one of those six behaviors is a place where hand-rolled versions get accessibility wrong, and they get it wrong quietly, and the person affected doesn’t file a bug. Focus not returning to the trigger is on approximately every custom dropdown I have ever tested, including several of mine.

So this isn’t a convenience feature with an accessibility side benefit. It’s an accessibility fix that happens to also delete 180 lines, and I’d rather it were described that way round.

Read similar posts
6 min

Heading levels are an outline

On a page I audited last month the only `h1` was the logo, the section headings were all `h4` because `h4` was the right size, and the actual page title was a `div` with a class on it.

6 min

details and summary

An FAQ page arrived on my desk with about 200 lines of JavaScript running an accordion, and the two elements that do it natively have been in every browser since 2020 and got the missing feature last winter.