Picture-in-Picture for any Element, not just <video>

François Beaufort
François Beaufort

Browser Support

  • Chrome: 116.
  • Edge: 116.
  • Firefox: not supported.
  • Safari: not supported.

Source

The Document Picture-in-Picture API makes it possible to open an always-on-top window that can be populated with arbitrary HTML content. It extends the existing Picture-in-Picture API for <video> that only allows an HTML <video> element to be put into a Picture-in-Picture window.

The Picture-in-Picture window in the Document Picture-in-Picture API is similar to a blank same-origin window opened via window.open(), with some differences:

  • The Picture-in-Picture window floats on top of other windows.
  • The Picture-in-Picture window never outlives the opening window.
  • The Picture-in-Picture window cannot be navigated.
  • The Picture-in-Picture window position cannot be set by the website.
A Picture-in-Picture window playing Sintel trailer video.
A Picture-in-Picture window created with the Document Picture-in-Picture API (demo).

Current status

Step Status
1. Create explainer Complete
2. Create initial draft of specification In progress
3. Gather feedback & iterate on design In progress
4. Origin trial Complete
5. Launch Complete (Desktop)

Use cases

Custom video player

A website can provide a Picture-in-Picture video experience with the existing Picture-in-Picture API for <video>, however it is very limited. The existing Picture-in-Picture window accepts few inputs, and has limited ability for styling them. With a full Document in Picture-in-Picture, the website can provide custom controls and inputs (for example, captions, playlists, time scrubber, liking and disliking videos) to improve the user's Picture-in-Picture video experience.

Video conferencing

It is common for users to leave the browser tab during a video conferencing session for various reasons (for example, presenting another tab to the call or multitasking) while still wishing to see the call, so it's a prime use case for Picture-in-Picture. Once again, the current experience a video conferencing website can provide via the Picture-in-Picture API for <video> is limited in style and input. With a full Document in Picture-in-Picture, the website can easily combine multiple video streams into a single PiP window without having to rely on canvas hacks and provide custom controls such as sending a message, muting another user, or raising a hand.

Productivity

Research has shown that users need more ways to be productive on the web. Document in Picture-in-Picture gives web apps the flexibility to accomplish more. Whether it's text editing, note-taking, task lists, messaging and chat, or design and development tools, web apps can now keep their content always accessible.

Interface

Properties

documentPictureInPicture.window
Returns the current Picture-in-Picture window if any. Otherwise, returns null.

Methods

documentPictureInPicture.requestWindow(options)

Returns a promise that resolves when a Picture-in-Picture window is opened. The promise rejects if it's called without a user gesture. The options dictionary contains the optional following members:

width
Sets the initial width of the Picture-in-Picture window.
height
Sets the initial height of the Picture-in-Picture window.
disallowReturnToOpener
Hides the "back to tab" button in the Picture-in-Picture window if true. It is false by default.
preferInitialWindowPlacement
Open the Picture-in-Picture window in its default position and size if true. It is false by default.

Events

documentPictureInPicture.onenter
Fired on documentPictureInPicture when a Picture-in-Picture window is opened.

Examples

The following HTML sets up a custom video player and a button element to open the video player in a Picture-in-Picture window.

<div id="playerContainer">
  <div id="player">
    <video id="video"></video>
  </div>
</div>
<button id="pipButton">Open Picture-in-Picture window</button>

Open a Picture-in-Picture window

The following JavaScript calls documentPictureInPicture.requestWindow() when the user clicks the button to open a blank Picture-in-Picture window. The returned promise resolves with a Picture-in-Picture window JavaScript object. The video player is moved to that window using append().

pipButton.addEventListener('click', async () => {
  const player = document.querySelector("#player");

  // Open a Picture-in-Picture window.
  const pipWindow = await documentPictureInPicture.requestWindow();

  // Move the player to the Picture-in-Picture window.
  pipWindow.document.body.append(player);
});

Set the size of the Picture-in-Picture window

To set the size of the Picture-in-Picture window, set the width and height options of documentPictureInPicture.requestWindow() to the desired Picture-in-Picture window size. Chrome may clamp the option values if they are too large or too small to fit a user-friendly window size.

pipButton.addEventListener("click", async () => {
  const player = document.querySelector("#player");

  // Open a Picture-in-Picture window whose size is
  // the same as the player's.
  const pipWindow = await documentPictureInPicture.requestWindow({
    width: player.clientWidth,
    height: player.clientHeight,
  });

  // Move the player to the Picture-in-Picture window.
  pipWindow.document.body.append(player);
});

Hide the "back to tab" button of the Picture-in-Picture window

To hide the button in the Picture-in-Picture window that allows the user to go back to the opener tab, set the disallowReturnToOpener option of documentPictureInPicture.requestWindow() to true.

pipButton.addEventListener("click", async () => {
  // Open a Picture-in-Picture window which hides the "back to tab" button.
  const pipWindow = await documentPictureInPicture.requestWindow({
    disallowReturnToOpener: true,
  });
});

Open the Picture-in-Picture window in its default position and size

To not reuse the position or size of the previous Picture-in-Picture window, set the preferInitialWindowPlacement option of documentPictureInPicture.requestWindow() to true.

pipButton.addEventListener("click", async () => {
  // Open a Picture-in-Picture window in its default position / size.
  const pipWindow = await documentPictureInPicture.requestWindow({
    preferInitialWindowPlacement: true,
  });
});

Copy style sheets to the Picture-in-Picture window

To copy all CSS style sheets from the originating window, loop through styleSheets explicitly linked into or embedded in the document and append them to the Picture-in-Picture window. Note that this is a one-time copy.

pipButton.addEventListener("click", async () => {
  const player = document.querySelector("#player");

  // Open a Picture-in-Picture window.
  const pipWindow = await documentPictureInPicture.requestWindow();

  // Copy style sheets over from the initial document
  // so that the player looks the same.
  [...document.styleSheets].forEach((styleSheet) => {
    try {
      const cssRules = [...styleSheet.cssRules].map((rule) => rule.cssText).join('');
      const style = document.createElement('style');

      style.textContent = cssRules;
      pipWindow.document.head.appendChild(style);
    } catch (e) {
      const link = document.createElement('link');

      link.rel = 'stylesheet';
      link.type = styleSheet.type;
      link.media = styleSheet.media;
      link.href = styleSheet.href;
      pipWindow.document.head.appendChild(link);
    }
  });

  // Move the player to the Picture-in-Picture window.
  pipWindow.document.body.append(player);
});

Handle when the Picture-in-Picture window closes

Listen to the window "pagehide" event to know when the Picture-in-Picture window gets closed (either because the website initiated it or the user manually closed it). The event handler is a good place to get the elements back out of the Picture-in-Picture window as shown below.

pipButton.addEventListener("click", async () => {
  const player = document.querySelector("#player");

  // Open a Picture-in-Picture window.
  const pipWindow = await documentPictureInPicture.requestWindow();

  // Move the player to the Picture-in-Picture window.
  pipWindow.document.body.append(player);

  // Move the player back when the Picture-in-Picture window closes.
  pipWindow.addEventListener("pagehide", (event) => {
    const playerContainer = document.querySelector("#playerContainer");
    const pipPlayer = event.target.querySelector("#player");
    playerContainer.append(pipPlayer);
  });
});

Close the Picture-in-Picture window programmatically by using the close() method.

// Close the Picture-in-Picture window programmatically. 
// The "pagehide" event will fire normally.
pipWindow.close();

Listen to when the website enters Picture-in-Picture

Listen to the "enter" event on documentPictureInPicture to know when a Picture-in-Picture window is opened. The event contains a window object to access the Picture-in-Picture window.

documentPictureInPicture.addEventListener("enter", (event) => {
  const pipWindow = event.window;
});

Access elements in the Picture-in-Picture window

Access elements in the Picture-in-Picture window either from the object returned by documentPictureInPicture.requestWindow(), or with documentPictureInPicture.window as shown below.

const pipWindow = documentPictureInPicture.window;
if (pipWindow) {
  // Mute video playing in the Picture-in-Picture window.
  const pipVideo = pipWindow.document.querySelector("#video");
  pipVideo.muted = true;
}

Handle events from the Picture-in-Picture window

Create buttons and controls and respond to user's input events such as "click" as you would do normally in JavaScript.

// Add a "mute" button to the Picture-in-Picture window.
const pipMuteButton = pipWindow.document.createElement("button");
pipMuteButton.textContent = "Mute";
pipMuteButton.addEventListener("click", () => { 
  const pipVideo = pipWindow.document.querySelector("#video");
  pipVideo.muted = true;
});
pipWindow.document.body.append(pipMuteButton);

Resize the Picture-in-Picture window

Use the resizeBy() and resizeTo() Window methods to resize Picture-in-Picture window. Both methods require a user gesture.

const resizeButton = pipWindow.document.createElement('button');
resizeButton.textContent = 'Resize';
resizeButton.addEventListener('click', () => {
  // Expand the Picture-in-Picture window's width by 20px and height by 30px.
  pipWindow.resizeBy(20, 30);
});
pipWindow.document.body.append(resizeButton);

Focus the opener window

Use the focus() Window method to focus the opener window from the Picture-in-Picture window. This method requires a user gesture.

const returnToTabButton = pipWindow.document.createElement("button");
returnToTabButton.textContent = "Return to opener tab";
returnToTabButton.addEventListener("click", () => {
  window.focus();
});
pipWindow.document.body.append(returnToTabButton);

CSS picture-in-picture display mode

Use the CSS picture-in-picture display mode to write specific CSS rules that are only applied when (part of the) the web app is shown in Picture-in-Picture mode.

@media all and (display-mode: picture-in-picture) {
  body {
    margin: 0;
  }
  h1 {
    font-size: 0.8em;
  }
}

Feature detection

To check if the Document Picture-in-Picture API is supported, use:

if ('documentPictureInPicture' in window) {
  // The Document Picture-in-Picture API is supported.
}

Demos

VideoJS player

You can play with the Document Picture-in-Picture API VideoJS player demo. Be sure to check out the source code.

Pomodoro

Tomodoro, a pomodoro web app, is also taking advantage of the Document Picture-in-Picture API when available (see GitHub pull request).

Screenshot of Tomodoro, a pomodoro web app.
A Picture-in-Picture window in Tomodoro.

Feedback

Please file issues on GitHub with suggestions and questions.

Acknowledgements

Hero image by Jakob Owens.