# Document Picture-in-Picture API

> documentPictureInPicture.requestWindow() opens an always-on-top window holding arbitrary HTML. Options, the exceptions it rejects with, examples with fallbacks.

`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`).

## Syntax

```js
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.

## Parameters

`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.

## Exceptions

`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.

:::observed
Chrome rejects a `requestWindow()` call made outside a user-activation handler with `NotAllowedError: Document PiP requires user activation`, a call that passes `{ width: 400 }` alone with `RangeError: Height must be specified if width is specified`, and a call from an iframe with `NotAllowedError: Opening a PiP window is only allowed from a top-level browsing context`. The strings are thrown in Chromium's [`picture_in_picture_controller_impl.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/document_picture_in_picture/picture_in_picture_controller_impl.cc) and [`document_picture_in_picture.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/document_picture_in_picture/document_picture_in_picture.cc) (chromium.googlesource.com).
:::

## Examples

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

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.

```js
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

`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.

```js
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.

## See also

- [Media Session API](/reference/capabilities/media-session/), whose `enterpictureinpicture` action handler is the only way to open the window without a click
- [Window Management API](/reference/capabilities/window-management/), for placing ordinary windows across screens
- [Screen Wake Lock API](/reference/capabilities/wake-lock/), for keeping the screen on while a floating player runs
- [Document Picture-in-Picture: requestWindow() method](https://wicg.github.io/document-picture-in-picture/#dom-documentpictureinpicture-requestwindow) (wicg.github.io)
- [Document Picture-in-Picture API](https://developer.chrome.com/docs/web-platform/document-picture-in-picture) (developer.chrome.com)