HTML&CSS

Creating Web Widgets Using the Document Picture-in-Picture API


Firefox 151 recently shipped the Document Picture-in-Picture API. This isn’t the same thing as the (regular?) Picture-in-Picture API, which pushes videos into a resizable window that remains visible even after switching browser tabs or OS windows. No, the Document Picture-in-Picture API enables us to put anything into the window.

I guess we can think of these windows as web widgets. We can use them for floating stock tickers, live chat conversations, playlists, to-do lists, notes, spreadsheets — anything that we’d want to keep on the screen at all times.

The general idea is that we create a Document Picture-in-Picture window (DPIP window), and then we put HTML, CSS, and JavaScript into it. It’s pretty simple when you think about it, but as we explore how the Document Picture-in-Picture API works, we’re going to tackle a slightly more complex scenario that you’ll probably run into.

We’re going to clone a stock ticker from the main document into a DPIP window. This not only gives us an opportunity to talk about some relevant media queries and pseudo-classes, which we’ll use to write targeted CSS for the DPIP window, but it’s also a stark reminder that taking a HTML component out of context can break the CSS, so you’ll need to keep that in mind.

This is said stock ticker:

But for it to work, you’ll need to open the demo in debug mode. This is because picture-in-picture doesn’t work in nested browsing contexts such as CodePen s.

In addition, Safari doesn’t support the DPIP API yet, so make sure that you’re using Chrome or Firefox.

Ready to begin?

The JavaScript of it all

First we need to check if the browser supports the Document Picture-in-Picture API. I imagine that it’d be a nice-to-have feature, so why wait for Safari support? Unfortunately though, there’s no way to query whether or not @media (display-mode: picture-in-picture) is supported using feature queries (@supports) because the at-rule() function is only supported by Chrome, and any plans to support preludes (that’s this part: (display-mode: picture-in-picture)) appear to have been dropped anyway.

To do this would’ve been awesome:

@supports at-rule(@media; display-mode: picture-in-picture) {
  /* DPIP supported */
}

Note: Safari Technology Preview 251 release notes do mention support for at-rule detection in @supports but it’s unclear when that will rollout. And Firefox 155 announced support for it just a day after this published.

Instead we have to check browser support using JavaScript, removing the button if DPIP isn’t supported, or making it create a DPIP window if it is supported):

if (!("documentPictureInPicture" in window)) {
  /* DPIP not supported (remove button) */
  document.querySelector("button").remove();
} else {
  /* DPIP supported (listen for button click) */
  document.querySelector("button").addEventListener("click", async () => {
    /* ... */
  });
}

Keep in mind that the Document Picture-in-Picture API is a desktop-only API, so the check above accounts for that too while illustrating exactly why a full-featured at-rule() function would be so useful.

As for creating the DPIP window, there’s one thing that we might want to do first — handle an existing DPIP window. DPIP windows replace existing DPIP windows, so we don’t need to worry about that part of it, but we do need to decide what happens if the button is clicked a second time. The code below closes the DPIP window if it’s already open, effectively making the button a toggle button:

document.querySelector("button").addEventListener("click", async () => {
  /* If the DPIP window is open, close it */
  if (window.documentPictureInPicture.window) {
    window.documentPictureInPicture.window.close();
  }
});

The problem is that focus always switches to the DPIP window, so toggling the DPIP window off might require two button clicks. One solution to that is cloning the button into the DPIP window, but the DPIP window already has a “Close” icon-button, so there’s no point in that. Personally, I wouldn’t do anything, letting subsequent button clicks recreate the DPIP window. In fact, if the user moves or resizes the DPIP window, subsequent button clicks will reset it to its original position and size (with the right options).

On that note, let’s talk about creating DPIP windows and said options. It’s pretty obvious what the width and height options do, but note that we can’t set one without the other, and if we don’t set either, the browser chooses. The preferInitialWindowPlacement option, if set to true, prevents the browser from saving the position and size of the DPIP window. The disallowReturnToOpener option (not used here), if set to true, hides the “Back to tab” icon-button (which does the same thing as the “Close” icon button, but also takes the user back to the originating tab).

/* Create the DPIP window */
const DPIP = await window.documentPictureInPicture.requestWindow({
  width: 600,
  height: 400,
  preferInitialWindowPlacement: true
});

The requestWindow() method of the DocumentPictureInPicture interface returns a promise (hence why we’re using async and await), which means that we can take care of everything else while the window is being prepared.

We can clone HTML into the DPIP window like this:

/* Select the component */
const stock = document.querySelector("#stock");

/* Clone the component and append it to the DPIP  */
DPIP.document.body.append(stock.cloneNode(true));

But to clone multiple elements, we’d need to take a different approach. This is what we’re going to do as we clone all s and s (and