Skip to content

Capabilities · API

Document Picture-in-Picture API

Published

window.documentPictureInPicture.requestWindow() opens a same-origin, always-on-top window whose document you fill with arbitrary HTML, where the older Picture-in-Picture API accepts a single <video> element. The window floats above other windows, closes when the opener closes, cannot be navigated, and is limited to one per tab.

Chrome 116 and Edge 116 ship it on desktop only; BCD records version_added: false for Chrome on Android. Firefox 151 added it on desktop and also records no Android support. Safari has no implementation (BCD api.DocumentPictureInPicture).

documentPictureInPicture.requestWindow()
documentPictureInPicture.requestWindow(options)
documentPictureInPicture.window
documentPictureInPicture.addEventListener("enter", listener)

requestWindow() returns a Promise<Window> that fulfils with the new window’s own Window object; its document starts as about:blank with the opener’s document base URL, so relative stylesheet and image URLs resolve against the opener. The window getter returns the last-opened Picture-in-Picture window while it is open and null otherwise. enter fires on documentPictureInPicture with a DocumentPictureInPictureEvent whose window property is the new window; it fires only for windows your own requestWindow() call opened.

requestWindow() takes one optional options argument, a DocumentPictureInPictureOptions dictionary. Every member is optional.

Member Type Required Description
width unsigned long long No Requested viewport width in CSS pixels. 0 (the default) lets the browser choose. The browser may clamp a value that is too large or too small.
height unsigned long long No Requested viewport height in CSS pixels, same rules as width. Setting one of width and height without the other is a RangeError.
disallowReturnToOpener boolean No Default false. When true, a hint that the browser should not show the “back to tab” affordance on the window. Chrome 124.
preferInitialWindowPlacement boolean No Default false. When true, the browser should use the requested size and default position instead of restoring the size and position of the previously closed Picture-in-Picture window. Chrome 130.

Both boolean members are hints; the specification uses “should” and “may”, so a browser that honours neither is conforming.

requestWindow() rejects its promise in the following cases, listed in the order the specification checks them.

Exception Condition
NotSupportedError The browser’s Document Picture-in-Picture support flag is false (for example the feature is disabled by policy).
NotAllowedError The calling window is not a top-level traversable (an iframe); or the caller is itself a Picture-in-Picture window; or the window has no transient user activation.
RangeError width is greater than zero but height is missing or zero, or the reverse.

Opening a second window from the same tab is not an error: the specification closes the previous Picture-in-Picture window first and resolves with the new one.

Every example checks "documentPictureInPicture" in window first and keeps the content in the main document when the check fails. The call to requestWindow() stays inside the click handler because it consumes the transient activation.

Popping a player out with its stylesheets, then docking it back

Section titled “Popping a player out with its stylesheets, then docking it back”

The new window starts empty, so copy the opener’s stylesheets before moving nodes into it. Listening for pagehide on the Picture-in-Picture window returns the player to its original container when the user closes the window.

const mainContainer = document.querySelector("#player-slot");
const player = document.querySelector("#player");
const popOut = document.querySelector("#pop-out");
if (!("documentPictureInPicture" in window)) {
popOut.hidden = true; // no pop-out on this browser; the player stays docked
}
popOut.addEventListener("click", async () => {
const pipWindow = await documentPictureInPicture.requestWindow({
width: 320,
height: 180,
});
for (const sheet of document.styleSheets) {
try {
const css = [...sheet.cssRules].map((rule) => rule.cssText).join("");
const style = document.createElement("style");
style.textContent = css;
pipWindow.document.head.append(style);
} catch {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = sheet.href; // cross-origin sheet: link it instead of reading it
pipWindow.document.head.append(link);
}
}
pipWindow.document.body.append(player);
pipWindow.addEventListener("pagehide", () => mainContainer.append(player));
});

Reading cssRules on a cross-origin stylesheet throws a SecurityError, which is why the catch branch links the sheet by URL instead.

Reusing the open window instead of opening a second one

Section titled “Reusing the open window instead of opening a second one”

documentPictureInPicture.window tells you whether a window is already open. Reusing it avoids the close-and-reopen flicker the specification mandates when requestWindow() is called while a window exists.

async function getPipWindow(button) {
if (!("documentPictureInPicture" in window)) return null;
const existing = documentPictureInPicture.window;
if (existing) return existing;
try {
return await documentPictureInPicture.requestWindow({
disallowReturnToOpener: true,
});
} catch (err) {
console.error(`${err.name}: ${err.message}`);
return null;
}
}
button.addEventListener("click", async () => {
const pipWindow = await getPipWindow(button);
if (!pipWindow) {
button.textContent = "Pop-out unavailable";
return;
}
pipWindow.document.body.textContent = `Opened at ${new Date().toLocaleTimeString()}`;
});

Returning null from the helper lets the caller render an in-page state instead of an unhandled rejection.

Specifications

SpecificationStatus
Document Picture-in-Picture: requestWindow() methodWICG draft
Document Picture-in-Picture: DocumentPictureInPictureOptions dictionaryWICG draft