Low angle photo of pink and orange balloons

Pop-ups: They're making a resurgence!

Published on

The goal of the Open UI initiative is to make it easier for developers to make great user experiences. To do this, we are trying to tackle the more problematic patterns that developers face. We can do this by providing better platform built-in APIs and components.

One such problem area is pop-ups.

Pop-ups have had a rather polarizing reputation for a long time. This is, in part, due to the way they get both built and deployed. They're not an easy pattern to build well, but they can yield a lot of value by directing users to certain things, or making them aware of the content on your site—especially when used in a tasteful manner.

There are often two major concerns when building pop-ups:

  • How to make sure it gets placed above the rest of your content in an appropriate place.
  • How to make it accessible (keyboard friendly, focusable, and so on).

The built-in pop-up API has a variety of goals, all with the same overarching goal of making it easy for developers to build this pattern. Notable of those goals are:

  • Make it easy to display an element and its descendants above the rest of the document.
  • Make it accessible.
  • Not require JavaScript for most common behaviors (light dismiss, singleton, stacking, and so on).

You can check out the full spec for pop-ups on the OpenUI site.

Browser compatibility

Where can you use the built-in pop-up API now? It's supported in Chrome Canary behind the "Experimental web platform features" flag at the time of writing.

To enable that flag, open Chrome Canary and visit chrome://flags. Then enable the "Experimental web platform features" flag.

There is an Origin Trial too for developers that would like to test this out in a production environment.

Lastly, there is a polyfill under development for the API. Be sure to check out the repo at github.com/oddbird/popup-polyfill.

You can check for pop-up support with:

const supported = Element.prototype.hasOwnProperty("popUp");

Current solutions

What can you currently do to promote your content above everything else? If it's supported in your browser, you could use the HTML Dialog element. You'd need to use it in "Modal" form. And this requires JavaScript to use.

Dialog.showModal();

There are some accessibility considerations. It's advised to use a11y-dialog for example if catering for users of Safari below version 15.4.

You could also use one of the many pop-up, alert, or tooltip based libraries out there. Many of these tend to work in a similar way.

  • Append some container to the body for showing pop-ups.
  • Style it so that it sits above everything else.
  • Create an element and append it to the container to show a pop-up.
  • Hide it by removing the pop-up element from the DOM.

This requires an extra dependency and more decisions for developers. It also requires research to find an offering that provides everything you need. The pop-up API, although named pop-up, aims to cater for many scenarios including tooltips. The goal being to cover all those common scenarios, saving developers from having to make yet another decision so they can focus on building their experiences.

Your first pop-up

This is all you need.

<div id="my-first-popup" popup>PopUp Content!</div>
<button popuptoggletarget="my-first-popup">Toggle Pop-Up</button>

But, what is happening here?

  • You don’t have to put the pop-up element into a container or anything—it's hidden by default.
  • You don’t have to write any JavaScript to make it appear. That gets handled by the popuptoggletarget attribute.
  • When it appears, it gets promoted to the top layer. That means it gets promoted above the document in the viewport. You don’t have to manage z-index or worry about where your pop-up is in the DOM. It could be deep down nested in the DOM, with clipping ancestors. You can also see which elements are currently in the top layer through DevTools. For more on the top layer, check out this article.

GIF of DevTools top layer support being demonstrated

  • You get "Light Dismiss" out of the box. By that, we mean you can close the pop-up with a close signal, such as clicking outside the pop-up, keyboard-navigating to another element, or pressing the Esc key. Open it up again and try it out!

What else do you get with pop-up? Let's take the example further. Consider this demo with some content on the page.

That floating action button has fixed positioning with a high z-index.

.fab {
position: fixed;
z-index: 99999;
}

The floating action button could also be a pop-up. But, here we are showing off the power of having access to the top layer. More on floating action buttons as pop-ups later.

The pop-up content is nested in the DOM, but when you open the pop-up, it gets promoted above that fixed position element. You don’t need to set any styles.

You may also notice that the pop-up now has a ::backdrop pseudo-element. All elements that are in the top layer get a styleable ::backdrop pseudo-element. This example styles ::backdrop with a reduced alpha background color and a backdrop filter, which blurs out the underlying content.

By default, the ::backdrop on a pop-up has pointer-events: none set.

Styling a pop-up

Let's turn our attention to styling the pop-up. By default, a pop-up has a fixed position and some applied padding. It also has display: none. You could override this to show a pop-up. But, that wouldn't promote it to the top layer.

[popup] { display: block; } 

If you want a pop-up to be shown on load, you can use the defaultopen attribute.

<div popup defaultopen id=”pop”>Pop-up content!</div>

Regardless of how you promote your pop-up, once you promote a pop-up to the top layer, you may need to lay it out or position it. You can't target the top layer and do something like

:open {
display: grid;
place-items: center;
}

By default, a pop-up will lay out in the center of the viewport using margin: auto. But, in some cases, you may want to be explicit about positioning. For example:

[popup] {
top: 50%;
left: 50%;
translate: -50%;
}

If you want to lay out content inside your pop-up using CSS grid or flexbox, it might be wise to wrap this in an element. Otherwise, you'll need to declare a separate rule that changes the display once the pop-up is in the top layer. Setting it by default would have it shown by default overriding display: none.

[popup]:open {
display: flex;
}

You could also use your root pop-up element as a container that fills the viewport too. Then use that to lay out content inside the top layer.

If you tried that demo out, you'll notice that the pop-up is now transitioning in and out. You can transition pop-ups in and out by using the :open pseudo-selector. The :open pseudo-selector matches pop-ups that are showing (and therefore in the top layer).

To check whether an element is open with JavaScript, use: element.matches(':open'). Note that this may change as the spec evolves.

This example uses a custom property to drive the transition. And you can apply a transition to the pop-up’s ::backdrop too.

[popup] {
--hide: 1;
transition: transform 0.2s;
transform: translateY(calc(var(--hide) * -100vh))
scale(calc(1 - var(--hide)));
}

[popup]::backdrop {
transition: opacity 0.2s;
opacity: calc(1 - var(--hide, 1));
}

[popup]:open,
[popup]:open::backdrop
{
--hide: 0;
}

A tip here is to group transitions and animations under a media query for motion. This can help to maintain your timings too. This is because you can't share values between the popup and the ::backdrop via custom property.

@media(prefers-reduced-motion: no-preference) {
[popup] { transition: transform 0.2s; }
[popup]::backdrop { transition: opacity 0.2s; }
}

Up until this point, you've seen the use of popuptoggletarget to show a pop-up. To dismiss it, we're using "Light dismiss". But, you also get popupshowtarget and popuphidetarget attributes you can use. Let's add a button to a pop-up that hides it and change the toggle button to use popupshowtarget.

<div id="code-pop-up" popup>
<button popuphidetarget="code-pop-up">Hide Code</button>
</div>
<button popupshowtarget="code-pop-up">Reveal Code</button>

You may also notice that the transition in and out has changed. It's now powered by separate animations to enter from one side and exit on the other. This means you can do things like enter on one axis and exit on another. Make sure you set animation-fill-mode if your ::backdrop and [popup] timings are different.

As mentioned earlier, the pop-up API covers more than only our historical notion of pop-ups. You could build for all types of scenarios such as notifications, menus, tooltips etc.

Some of those scenarios need different interaction patterns. Interactions like hover. The use of a popuphovertarget attribute was experimented with but isn't currently implemented.

You can get involved with the discussion about hover interactions for pop-ups here.

<div popuphovertarget="hover-pop-up">Hover for Code</div>

The idea being that you hover an element to show the target. This behavior could get configured via CSS properties. These CSS properties would define the window of time for hovering on and off an element that a pop-up reacts to. The default behavior experimented with had a pop-up show after an explicit 0.5s of :hover. Then it would need a light dismiss or the opening of another pop-up to dismiss (More on this coming up). This was due to the pop-up hide duration being set to Infinity.

The use of popuphovertarget is something not standardized or currently implemented.

In the meantime, you could use JavaScript to polyfill that functionality.

let hoverTimer;
const HOVER_TRIGGERS = document.querySelectorAll("[popuphovertarget]");
const tearDown = () => {
if (hoverTimer) clearTimeout(hoverTimer);
};
HOVER_TRIGGERS.forEach((trigger) => {
const popup = document.querySelector(
`#${trigger.getAttribute("popuphovertarget")}`
);
trigger.addEventListener("pointerenter", () => {
hoverTimer = setTimeout(() => {
if (!popup.matches(":open")) popup.showPopUp();
}, 500);
trigger.addEventListener("pointerleave", tearDown);
});
});

The benefit of setting something an explicit hover window is that it ensures the user’s action is intentional (for example, a user passes their pointer over a target). We don't want to show the pop-up unless that is their intention.

Try out this demo where you can hover the target with the window set to 0.5s.


Before exploring some common use cases and examples, let’s go over a few things.


Types of pop-up

We've covered non-JavaScript interaction behavior. But what about pop-up behavior as a whole. What if you don't want "Light dismiss"? Or you want to apply a singleton pattern to your pop-ups?

The pop-up API allows you to specify three types of pop-up which differ in behavior.

[popup=auto]/[popup]:

  • Nesting support. This doesn't only mean nested in the DOM either. The definition of an ancestral pop-up is one that is:
    • related by DOM position (child).
    • related by triggering attributes on child elements such as popuptoggletarget, popuphovertarget, and so on.
    • related by the anchor attribute (Under development CSS Anchoring API).
  • Light dismiss.
  • Opening dismisses other pop-ups that are not ancestral pop-ups. Have a play with the demo below that highlights how nesting with ancestral pop-ups works. See how changing some of the popuphidetarget/popupshowtarget instances to popuptoggletarget changes things.
  • Light dismissing one dismisses all, but dismissing one in the stack only dismisses those above it in the stack.

[popup=hint]

  • Singleton. Can only show one pop-up of type hint at a time. Other pop-up types remain open. Check out the demo below. Even though there are ancestral pop-ups, they’re dismissed when a different pop-up gets shown.
  • Light dismiss.
  • Can’t be shown by default with defaultopen.

[popup=manual]

  • Doesn't close other pop-ups.
  • No light dismiss.
  • Requires explicit dismiss via trigger element or JavaScript.

JavaScript API

When you need more control over your pop-ups, you can approach things with JavaScript. You get both a showPopUp and hidePopUp method. You also have show and hide events to listen for:

Show a pop-up

popUpElement.showPopUp()

Hide a pop-up:

popUpElement.hidePopUp()

Listen for a pop-up being shown:

popUpElement.addEventListener('show', doSomethingWhenPopUpShows)

Listen for a pop-up being shown and cancel it being shown:

popUpElement.addEventListener('show',event => {
event.preventDefault();
console.warn(‘We blocked a pop-up from being shown’);
})

Listen for a pop-up being hidden:

popUpElement.addEventListener('hide', doSomethingWhenPopUpHides)

You can't cancel a pop-up being hidden:

popUpElement.addEventListener('hide',event => {
event.preventDefault();
console.warn("You aren't allowed to cancel the hiding of a pop-up");
})

Check whether a pop-up is in the top layer:

popUpElement.matches(':open')

This provides extra power for some less common scenarios. For example, show a pop-up after a period of inactivity.

This demo has pop-ups with audible pops, so we'll need JavaScript to play the audio. On click, we are hiding the pop-up, playing the audio, and then showing it again.

Accessibility

Accessibility is at the forefront of thinking with the pop-up API. Accessibility mappings associate the pop-up with its trigger element, as needed. This means you don't need to declare aria-* attributes such as aria-haspopup, assuming you use one of the triggering attributes like popuptoggletarget.

For focus management, you can use the autofocus attribute to move focus to an element inside a pop-up. This is the same as for a Dialog, but the difference comes when returning focus, and that's because of light dismiss. In most cases, closing a pop-up returns focus to the previously focused element. But focus gets moved to a clicked element on light dismiss, if it can get focus. Check out the section about focus management in the explainer.

You'll need to open the "full screen version" of this demo to see it work.

In this demo, the focussed element gets a green outline. Try tabbing around the interface with your keyboard. Note where the focus gets returned when a pop-up gets closed. You may also notice that if you tabbed about, the pop-up closed. That's by design. Although pop-ups have focus management, they don't trap focus. And keyboard navigation identifies a close signal when the focus moves out of the pop-up.

Anchoring (under development)

When it comes to pop-ups, a tricky pattern to cater for is anchoring the element to its trigger. For example, if a tooltip is set to show above its trigger but the document gets scrolled. That tooltip could get cut off by the viewport. There are current JavaScript offerings to deal with this such as "Floating UI". They will reposition the tooltip for you to stop this happening and rely on a desired position order.

But, we want you to be able to define this with your styles. There is a companion API under development alongside the pop-up API to tackle this. The "CSS Anchor Positioning" API will allow you to tether elements to other elements, and it will do this in a manner that re-positions elements so that they aren't cut off by the viewport.

This demo uses the Anchoring API in its current state. The position of the boat responds to the anchor's position in the viewport.

Here's a snippet of the CSS making this demo work. No JavaScript required.

.anchor {
--anchor-name: --anchor;
}
.anchored {
position: absolute;
position-fallback: --compass;
}
@position-fallback --compass {
@try {
bottom: anchor(--anchor top);
left: anchor(--anchor right);
}
@try {
top: anchor(--anchor bottom);
left: anchor(--anchor right);
}
}

You can check out the spec here. There will also be a polyfill for this API.

Examples

Now you’re familiar with what pop-up has to offer and how, let’s dig into some examples.

Notifications

This demo shows a "Copy to clipboard" notification.

  • Uses [popup=manual].
  • On action show pop-up with showPopUp.
  • After a 2000ms timeout, hide it with hidePopUp.

Toasts

This demo uses the top layer to show toast style notifications.

  • One pop-up with type manual acts as the container.
  • New notifications are appended to the pop-up and the pop-up is shown.
  • They're removed with the web animations API on click and removed from the DOM.
  • If there are no toasts to show, the pop-up is hidden.

Nested menu

This demo shows how a nested navigation menu could work.

  • Use [popup=auto] as it allows nested pop-ups.
  • Use autofocus on the first link of each dropdown in order to keyboard navigate.
  • This is a perfect candidate for the CSS Anchoring API. But, for this demo you can use a small amount of JavaScript to update the positions using custom properties.
const ANCHOR = (anchor, anchored) => () => {
const { top, bottom, left, right } = anchor.getBoundingClientRect();
anchored.style.setProperty("--top", top);
anchored.style.setProperty("--right", right);
anchored.style.setProperty("--bottom", bottom);
anchored.style.setProperty("--left", left);
};

PRODUCTS_MENU.addEventListener("show", ANCHOR(PRODUCT_TARGET, PRODUCTS_MENU));

Remember, because this demo uses autofocus, it will need to be opened in "full screen view" for keyboard navigation.

Media pop-up

This demo shows how you might pop media up.

  • Uses [popup=auto] for light dismiss.
  • JavaScript listens for the video's play event and pops the video up.
  • The pop-ups hide event pauses the video.

Wiki style pop-ups

This demos shows how you might create inline content tooltips that contain media.

  • Uses [popup=hint] for singleton pattern pop-ups. Showing one hides the others.
  • Shown on pointerenter with JavaScript.
  • Another perfect candidate for the CSS Anchoring API.

This demo creates a navigation drawer using a pop-up.

  • Uses [popup=auto] for light dismiss.
  • Uses autofocus to focus the first navigation item.

Managing backdrops

This demo shows how you might manage backdrops for mutliple pop-ups where you only want one ::backdrop to be visible.

  • Use JavaScript to maintain a list of the pop-ups that are visible.
  • Apply a class name to the lowest pop-up in the top layer.

Custom cursor pop-up

This demo shows how to use popup to promote a canvas to the top layer and use it to show a custom cursor.

  • Promote canvas to top layer with defaultopen and [popup=manual].
  • When other pop-ups are opened, hide and show the canvas pop-up to make sure it's on top.

Actionsheet pop-up

This demo shows how you could use a pop-up as an actionsheet.

  • Have the pop-up shown by default overriding display.
  • Actionsheet is opened with the pop-up trigger.
  • When the pop-up is shown, it is promoted to the top layer and translated into view.
  • Light dismiss can be used to return it.

Keyboard activated pop-up

This demo shows how you could use pop-up for command palette style UI.

  • Use cmd + j to show the pop-up.
  • The input is focused with autofocus.
  • The combo box is a second pop-up positioned under the main input.
  • Light dismiss closes the palette if the dropdown is not present.
  • Another candidate for the Anchoring API

Timed pop-up

This demo shows an inactivity pop-up after four seconds. A UI pattern often used in apps that hold secure information about a user to show a logout modal.

  • Use JavaScript to show the pop-up after a period of inactivity.
  • On pop-up show, reset the timer.

Screensaver

Similar to the previous demo, you could add a dash of whimsy to your site and add a screensaver.

  • Use JavaScript to show the pop-up after a period of inactivity.
  • Light dismiss to hide and reset the timer.

Caret follow

This demo shows how you could have a pop-up follow an input caret.

  • Show the pop-up based on selection, key event, or special character input.
  • Use JavaScript to update the pop-up position with scoped custom properties.
  • This pattern would require considerate thought towards content being shown and accessibility.
  • It's often seen in text editing UI and apps where you can tag.

Floating action button menu

This demo shows how you could use pop-up to implement a floating action button menu without JavaScript.

  • Have a manual type pop-up with the defaultopen attribute set. This is the main button.
  • The menu is another pop-up that is the target of the main button.
  • Menu is opened with popuptoggletarget.
  • Use autofocus to focus the first menu item on show.
  • Light dismiss closes the menu.
  • The icon twist uses :has(). You can read more about :has() in this article.

That's it!

So, that’s an intro to pop-up, coming down the road as part of the Open UI initiative. Used sensibly, it’s going to be a fantastic addition to the web platform.

Be sure to check out Open UI. The pop-up explainer is kept up to date as the API evolves. And here's the collection for all the demos.

Thanks for “popping” by!


Photo by Madison Oren on Unsplash

Last updated: Improve article

We serve cookies on this site to analyze traffic, remember your preferences, and optimize your experience.