# Use picture-in-picture

Pop the video into a floating window, with availability detection and state that tracks every entry path.

> **Note**
>
> Using a pre-built [skin](./skins.md)? It already includes the controls shown here. You may still need the media or player setup in this guide. The component examples are for building your own player UI from individual [components](./ui-components.md).

## Recommended approach

Add a [`<media-pip-button>`](../reference/components/pip-button.md). It requests and exits picture-in-picture, reflects the current state, and renders nothing when picture-in-picture is unsupported.

**index.html**

```html
<video-player class="video-player">
  <media-container>
    <video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" autoplay muted playsinline loop></video>
    <media-pip-button class="media-pip-button">
      <span class="pip">Exit PiP</span>
      <span class="not-pip">Enter PiP</span>
    </media-pip-button>
  </media-container>
</video-player>
```

**index.css**

```css
.video-player media-container {
  position: relative;
  display: block;
}

.video-player media-container video {
  width: 100%;
}

.media-pip-button {
  position: absolute;
  right: 10px;
  bottom: 10px;
  padding-block: 8px;
  padding-inline: 20px;
  color: black;
  cursor: pointer;
  background: rgba(255, 255, 255, 0.7);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 9999px;
  backdrop-filter: blur(10px);
}

.media-pip-button .pip {
  display: none;
}
.media-pip-button .not-pip {
  display: none;
}
.media-pip-button[data-pip] .pip {
  display: inline;
}
.media-pip-button:not([data-pip]) .not-pip {
  display: inline;
}
```

**index.ts**

```ts
import '@videojs/html/video/player';
import '@videojs/html/ui/container';
import '@videojs/html/ui/pip-button';
```

## How it works

The [picture-in-picture feature](../reference/api/feature-pip.md) puts three things in player state.

`pip` is `true` while the video is in a floating window. It tracks every way in and out — your button, the native controls, or a control the browser adds on its own.

`pipAvailability` answers “can I offer this?” It starts as `'unavailable'`. Once the feature attaches to a media element, it settles to `'available'` or `'unsupported'`.

`requestPictureInPicture()`, `exitPictureInPicture()`, and `togglePictureInPicture()` change the presentation. If the browser blocks a request, the returned promise rejects. In a browser with no picture-in-picture API, the actions resolve without changing anything.

Picture-in-picture and fullscreen are exclusive: requesting one exits the other first.

## Availability and constraints

- Show picture-in-picture controls only when `pipAvailability` is `'available'`. `<media-pip-button>` handles this for you.
- Requests must come from a user gesture; browsers reject requests outside one.
- Firefox 153 (July 2026) added the programmatic picture-in-picture API. Earlier versions, including Firefox ESR 140, report `'unsupported'` even though they offer their own built-in picture-in-picture toggle to users.
- On iOS, picture-in-picture is supported on iPhone and iPad through the WebKit presentation mode; the player handles this path for you.
- The browser owns the floating window: its size, position, and built-in controls aren’t yours to style.
- The user can close the window or return the video at any time. React to `pip` state instead of assuming your button is the only entry and exit.

## Common variations

### Request picture-in-picture from app logic

Enter picture-in-picture when the user scrolls the player out of view or navigates within your app. This still requires a recent user gesture in most browsers, so tie it to an interaction:

```html
<video-player>
  <media-container>
    <video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" muted playsinline></video>
  </media-container>
</video-player>
<button type="button" id="watch-elsewhere">Keep watching while you browse</button>
<script type="module">
  import '@videojs/html/video/player';

  document.querySelector('#watch-elsewhere').addEventListener('click', () => {
    const video = document.querySelector('video');
    if (typeof video.requestPictureInPicture !== 'function') return; // Unsupported.
    video.requestPictureInPicture().catch(() => {
      // Rejected: no video data yet, or blocked by the browser.
    });
  });
</script>
```

## Troubleshooting

### The picture-in-picture button doesn’t render

`pipAvailability` is `'unsupported'` — the browser has no programmatic picture-in-picture API (Firefox before 153), or the media isn’t a video.

### The request rejects

The video has no data yet (wait for it to be ready), the call wasn’t tied to a user gesture, or the browser blocked it. Handle the rejection rather than assuming entry succeeded.

### State is wrong after the user closes the floating window

Read `pip` from player state instead of tracking entry and exit yourself; it stays in sync with every path in and out of picture-in-picture.

## Related pages

### Components

- [media-pip-button](../reference/components/pip-button.md): Accessible picture-in-picture toggle button with keyboard support and state reflection

### API

- [Picture-in-picture](../reference/api/feature-pip.md): Picture-in-picture state and actions for the player store

### Guides

- [Go fullscreen and lock orientation](./fullscreen.md): Present the player fullscreen with custom controls intact, and lock screen orientation on mobile.
- [Cast to AirPlay and Chromecast](./casting.md): Send playback to AirPlay and Google Cast devices, with availability detection and connection state.