# 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](https://videojs.org/docs/framework/react/guides/skins)? 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](https://videojs.org/docs/framework/react/guides/ui-components).

## Recommended approach

Add a [PiPButton](https://videojs.org/docs/framework/react/reference/components/pip-button). It requests and exits picture-in-picture, reflects the current state, and renders nothing when picture-in-picture is unsupported.

**App.tsx**

```tsx
import { Container, createPlayer, PiPButton } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';

const { Player } = createPlayer({ features: videoFeatures });

export default function BasicUsage() {
  return (
    <Player>
      <Container className="media-container">
        <Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" autoPlay muted playsInline loop />
        <PiPButton
          className="media-pip-button"
          render={(props, state) => <button {...props}>{state.pip ? 'Exit PiP' : 'Enter PiP'}</button>}
        />
      </Container>
    </Player>
  );
}
```

**App.css**

```css
.media-container {
  position: relative;
}

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

## How it works

The [picture-in-picture feature](https://videojs.org/docs/framework/react/reference/api/feature-pip) 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'`. PiPButton 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:

```tsx
import { Container } from '@videojs/react';
import { usePlayer, Video, VideoPlayer } from '@videojs/react/video';

function WatchElsewhereButton() {
  const store = usePlayer();

  return (
    <button
      type="button"
      onClick={() => {
        store.requestPictureInPicture().catch(() => {
          // Rejected: no video data yet, or blocked by the browser.
        });
      }}
    >
      Keep watching while you browse
    </button>
  );
}

export default function App() {
  return (
    <VideoPlayer>
      <Container>
        <Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" muted playsInline />
        <WatchElsewhereButton />
      </Container>
    </VideoPlayer>
  );
}
```

## 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

- [PiPButton](https://videojs.org/docs/framework/react/reference/components/pip-button): Accessible picture-in-picture toggle button with keyboard support and state reflection

### API

- [Picture-in-picture](https://videojs.org/docs/framework/react/reference/api/feature-pip): Picture-in-picture state and actions for the player store

### Guides

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

---

React documentation: https://videojs.org/docs/framework/react/llms.txt
All documentation: https://videojs.org/llms.txt
