Skip to content

Document Picture-in-Picture: always-on-top window

Published

In one line: per MDN, “the Document Picture-in-Picture API makes it possible to open an always-on-top window that can be populated with arbitrary HTML content,” extending the earlier Picture-in-Picture API that only supports a single <video> element.

MDN describes the Document Picture-in-Picture window as similar to a same-origin window opened with Window.open(), with a few differences: it “floats on top of other windows,” it “never outlives the opening window,” it “cannot be navigated,” and “the Picture-in-Picture window position cannot be set by the website.” MDN also notes the window “is limited to one per browser tab at a time.” MDN’s listed use cases include an always-on-top custom video player, a video-conferencing control surface, and always-visible productivity tools such as timers or to-do lists.

Per MDN’s browser-compat-data, the DocumentPictureInPicture interface and its requestWindow() method are supported in Chrome from version 116 (Edge mirrors Chrome) and in Firefox from version 151. The same data records the feature as unsupported in Safari, and unsupported on Chrome for Android and Firefox for Android (version_added: false in each case).

Per MDN, the entry point is Window.documentPictureInPicture, and the window is opened by calling DocumentPictureInPicture.requestWindow(), which “returns a Promise that fulfills with the window’s own Window object.” Per the requestWindow() reference, the call “requires transient activation,” so it must run inside a user-gesture handler such as a click listener, and it accepts an options object with width, height, disallowReturnToOpener, and preferInitialWindowPlacement:

// Create the content that will move into the Picture-in-Picture window, and the
// container it lives in while docked in the main document.
const mainContainer = document.createElement("div");
const playerContainer = document.createElement("div");
playerContainer.textContent = "Now playing…";
mainContainer.append(playerContainer);
document.body.append(mainContainer);
const button = document.createElement("button");
button.textContent = "Pop out player";
document.body.append(button);
button.addEventListener("click", async () => {
if (!("documentPictureInPicture" in window)) {
// No Document Picture-in-Picture support — leave the player docked in the
// main document instead of throwing.
return;
}
const pipWindow = await documentPictureInPicture.requestWindow({
width: 320,
height: 180,
});
// Copy the app's stylesheets so moved content keeps its styling.
[...document.styleSheets].forEach((sheet) => {
try {
const cssRules = [...sheet.cssRules].map((rule) => rule.cssText).join("");
const style = document.createElement("style");
style.textContent = cssRules;
pipWindow.document.head.appendChild(style);
} catch (e) {
// Skip any stylesheet we can't read here.
}
});
pipWindow.document.body.append(playerContainer);
pipWindow.addEventListener("pagehide", () => {
// Move the content back into the main window when the PiP window closes.
mainContainer.append(playerContainer);
});
});

Feature-detect documentPictureInPicture on window before calling requestWindow(), and fall back to a no-op when it is absent — the click handler above already guards the call this way, but the check can be reused standalone wherever the app needs to know up front whether pop-out is available (for example, to hide the “Pop out player” button entirely):

function supportsDocumentPictureInPicture() {
return "documentPictureInPicture" in window;
}
if (!supportsDocumentPictureInPicture()) {
// No Document Picture-in-Picture support — hide any UI that offers pop-out and
// keep everything in the main document instead.
document.querySelectorAll("[data-requires-document-pip]").forEach((el) => {
el.hidden = true;
});
}
  • requestWindow() requires transient activation — call it only from inside a user-gesture handler such as a click listener, per the MDN requestWindow() reference.
  • The Picture-in-Picture window “never outlives the opening window” and “cannot be navigated,” per MDN — do not rely on it as an independent, long-lived window.
  • Stylesheets are not copied automatically; append <style> or <link> elements to the Picture-in-Picture window’s own document.head for the moved content to render correctly.
  • MDN’s browser-compat-data records this as unsupported on Chrome for Android and Firefox for Android as well as on Safari, so feature-detect on every platform rather than assuming desktop-only gaps.
  • Per MDN, the enter event only fires when your own code opens the window programmatically; when the browser itself moves content into the window (for example because the tab was switched), that event does not fire — use a MediaSession action handler with a type of enterpictureinpicture instead.