Using the Document Picture-in-Picture API
Experimental: This is an experimental technology
Check the Browser compatibility table carefully before using this in production.
Secure context: This feature is available only in secure contexts (HTTPS), in some or all supporting browsers.
This guide provides a walkthrough of typical usage of the Document Picture-in-Picture API.
Note: You can see the featured demo in action at Document Picture-in-Picture API Example (see the full source code also).
Sample HTML
The following HTML sets up a basic video player.
<div id="container">
<p class="in-pip-message">
Video player is currently in the separate Picture-in-Picture window.
</p>
<div id="player">
<video
src="assets/bigbuckbunny.mp4"
id="video"
controls
width="320"></video>
<div id="credits">
<a href="https://peach.blender.org/download/" target="_blank">
Video by Blender </a
>;
<a href="https://peach.blender.org/about/" target="_blank">
licensed CC-BY 3.0
</a>
</div>
<div id="control-bar">
<p class="no-picture-in-picture">
Document Picture-in-Picture API not available
</p>
<p></p>
</div>
</div>
</div>
Feature detection
To check if the Document Picture-in-Picture API is supported, you can test whether documentPictureInPicture
is available on window
:
if ("documentPictureInPicture" in window) {
document.querySelector(".no-picture-in-picture").remove();
const togglePipButton = document.createElement("button");
togglePipButton.textContent = "Toggle Picture-in-Picture";
togglePipButton.addEventListener("click", togglePictureInPicture, false);
document.getElementById("control-bar").appendChild(togglePipButton);
}
If it is available, we remove the "Document Picture-in-Picture API not available" message and instead add a <button>
element to open the video player in a Document Picture-in-Picture window.
Open a Picture-in-Picture window
The following JavaScript calls window.documentPictureInPicture.requestWindow()
to open a blank Picture-in-Picture window. The returned Promise
fulfills with a Picture-in-Picture Window
object. The video player is moved to that window using Element.append()
, and we display the message informing the user that it has been moved.
The width
and height
options of requestWindow()
set the Picture-in-Picture window to the desired size. Browsers may clamp the option values if they are too large or too small to fit a user-friendly window size.
async function togglePictureInPicture() {
// Early return if there's already a Picture-in-Picture window open
if (window.documentPictureInPicture.window) {
return;
}
// Open a Picture-in-Picture window.
const pipWindow = await window.documentPictureInPicture.requestWindow({
width: videoPlayer.clientWidth,
height: videoPlayer.clientHeight,
});
// ...
// Move the player to the Picture-in-Picture window.
pipWindow.document.body.append(videoPlayer);
// Display a message to say it has been moved
inPipMessage.style.display = "block";
}
Copy style sheets to the Picture-in-Picture window
To copy all CSS style sheets from the originating window, loop through all style sheets explicitly linked into or embedded in the document (via Document.styleSheets
) and append them to the Picture-in-Picture window. Note that this is a one-time copy.
// ...
// 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);
}
});
// ...
Target styles when in Picture-in-Picture mode
The picture-in-picture
value of the display-mode
media feature allows developers to apply CSS to a document based on whether it is being displayed in Picture-in-Picture mode. Basic usage looks like so:
@media (display-mode: picture-in-picture) {
body {
background: red;
}
}
This snippet will turn the background of the document <body>
red, only when it is displayed in Picture-in-Picture mode.
In our demo, we combine the display-mode: picture-in-picture
value with the prefers-color-scheme
media feature to create light and dark color schemes that are applied based on the user's color scheme preference, only when the app is being shown in Picture-in-Picture mode.
@media (display-mode: picture-in-picture) and (prefers-color-scheme: light) {
body {
background: antiquewhite;
}
}
@media (display-mode: picture-in-picture) and (prefers-color-scheme: dark) {
body {
background: #333;
}
a {
color: antiquewhite;
}
}
Handle when the Picture-in-Picture window closes
The code for toggling the Picture-in-Picture window closed again when the button is pressed a second time looks like this:
inPipMessage.style.display = "none";
playerContainer.append(videoPlayer);
window.documentPictureInPicture.window.close();
Here we reverse the DOM changes — hiding the message and putting the video player back in the player container in the main app window. We also close the Picture-in-Picture window programmatically using the Window.close()
method.
However, you also need to consider the case where the user closes the Picture-in-Picture window by pressing the browser supplied close (X) button on the window itself. You can handle this by detecting when the window closes using the pagehide
event:
pipWindow.addEventListener("pagehide", (event) => {
inPipMessage.style.display = "none";
playerContainer.append(videoPlayer);
});
Listen to when the website enters Picture-in-Picture
Listen to the enter
event on the DocumentPictureInPicture
instance to know when a Picture-in-Picture window is opened.
In our demo, we use the enter
event to add a mute toggle button to the Picture-in-Picture window:
documentPictureInPicture.addEventListener("enter", (event) => {
const pipWindow = event.window;
console.log("Video player has entered the pip window");
const pipMuteButton = pipWindow.document.createElement("button");
pipMuteButton.textContent = "Mute";
pipMuteButton.addEventListener("click", () => {
const pipVideo = pipWindow.document.querySelector("#video");
if (!pipVideo.muted) {
pipVideo.muted = true;
pipMuteButton.textContent = "Unmute";
} else {
pipVideo.muted = false;
pipMuteButton.textContent = "Mute";
}
});
pipWindow.document.body.append(pipMuteButton);
});
Note:
The DocumentPictureInPictureEvent
event object contains a window
property to access the Picture-in-Picture window.
Access elements and handle events
You can access elements in the Picture-in-Picture window in several different ways:
- The
Window
instance returned by theDocumentPictureInPicture.requestWindow()
method, as seen above. - Via the
window
property of theDocumentPictureInPictureEvent
event object (on theenter
event), as seen above. - Via the
DocumentPictureInPicture.window
property:
const pipWindow = window.documentPictureInPicture.window;
if (pipWindow) {
// Mute video playing in the Picture-in-Picture window.
const pipVideo = pipWindow.document.querySelector("#video");
pipVideo.muted = true;
}
Once you've got a reference to the Picture-in-Picture window
instance, you can manipulate the DOM (for example creating buttons) and respond to user input events (such as click
) as you would do normally in the regular browser window context.