How to Implement Picture-in-Picture Without Breaking UX

Video editing setup with dual monitors showing Picture-in-Picture feature

Picture-in-Picture started as a TV gimmick. Now it’s baked into every modern browser and mobile OS. I still see developers treat it like a checkbox feature—just fire the API and move on. That’s a mistake. A solid PiP implementation respects how people actually split their attention. It should feel like a quiet, clever extension of the player, not a loud interruption. I’ll walk through the technical bits while keeping the user’s headspace front and center.

Understanding the PiP Mindset

People reach for PiP for one reason: they want to keep watching while doing something else. Maybe a coding tutorial is running while they type, or a live stream is murmuring in the corner as they browse. The floating window isn’t a tiny player plastered over everything. It’s a persistent, focused view that asks for almost no space and no interaction unless the viewer decides to engage.

From a UX engineering angle, that means three hard rules. One—the jump into PiP must be predictable. Two—the floating window should carry only the controls that truly matter. Three—the main page can’t turn into a broken mess when the video leaves. I’ll tackle each one with real code.

Starting with the Browser API

At the center sits the Picture-in-Picture Web API. Chromium browsers have it. Safari on desktop and iOS added support more recently. Firefox hides it behind a flag, so you’ll still want a fallback. The basic flow looks almost too easy:

const video = document.getElementById('my-video');
const pipButton = document.getElementById('pip-button');

pipButton.addEventListener('click', async () => {
  try {
    if (document.pictureInPictureElement) {
      await document.exitPictureInPicture();
    } else {
      await video.requestPictureInPicture();
    }
  } catch (error) {
    console.error('PiP failed:', error);
  }
});

That snippet toggles PiP on a click. Ship it alone, though, and you’ve already broken the experience. The button stays clickable even when PiP isn’t available. There’s zero visual feedback during the switch. Let’s fix that.

Detecting Capability and Showing State

Before anything else, check document.pictureInPictureEnabled. It’s a boolean that tells you if the browser can use the feature right this second. It’ll return false if the user turned it off in settings, or if the platform blocks it—common inside iframes that lack the right permissions.

if (!document.pictureInPictureEnabled) {
  pipButton.hidden = true;
} else {
  pipButton.addEventListener('click', handlePiPToggle);
  video.addEventListener('enterpictureinpicture', () => {
    pipButton.classList.add('active');
  });
  video.addEventListener('leavepictureinpicture', () => {
    pipButton.classList.remove('active');
  });
}

I also disable the button for a split second during the request to block double-clicks. Use the promise from requestPictureInPicture() to turn it back on. A tiny detail, but it stops someone from hammering a button that’s already working, which leads to weird state mismatches.

Person watching a coding tutorial in a floating Picture-in-Picture window while working on laptop

Designing the Transition Experience

A sudden leap from a full player to a tiny floating rectangle is jarring. The browser does the animation—usually a smooth shrink—but you can make the whole thing feel faster by prepping the page before PiP kicks in.

When the video enters PiP, the original element pauses and the browser drops in a placeholder. If your layout collapses awkwardly, you’ll get a flash of broken grids or overlapping text. I add a transition class to the video’s parent container. Say your page uses a grid—you can swap the area to a fixed height or hide non-essential elements while PiP is active.

video.addEventListener('enterpictureinpicture', () => {
  document.body.classList.add('pip-active');
});
video.addEventListener('leavepictureinpicture', () => {
  document.body.classList.remove('pip-active');
});

In your CSS, lean on that class to adjust things. Maybe the video container stays visible but drops its opacity and shows a calm “Playing in picture-in-picture” label. That tells the user exactly what’s going on. They can click the placeholder to pull the video back to its original spot—a small touch that avoids confusion.

Customizing the PiP Window Controls

The default PiP window gives you a play/pause button and a back-to-tab button. You can add more through the Media Session API. This is where I see people go overboard. They stuff the tiny window with actions. Don’t. Stick to the bare minimum.

if ('mediaSession' in navigator) {
  navigator.mediaSession.setActionHandler('play', () => {
    video.play();
  });
  navigator.mediaSession.setActionHandler('pause', () => {
    video.pause();
  });
  navigator.mediaSession.setActionHandler('previoustrack', () => {
    // Skip to previous chapter, if applicable
  });
  navigator.mediaSession.setActionHandler('nexttrack', () => {
    // Skip to next chapter, if applicable
  });
}

For tutorials or podcasts, previous/next track handlers make sense. For a long movie, maybe only play/pause belongs. I’ve seen players add a “like” button or a full-screen toggle—those don’t belong in a window the size of a business card. Every extra control invites accidental taps and annoyance.

Also update the metadata so the PiP window shows a helpful title and artwork. This matters more when people might have multiple PiP windows open (though most browsers cap it at one).

navigator.mediaSession.metadata = new MediaMetadata({
  title: 'CSS Grid Deep Dive',
  artist: 'Luca Fontana',
  album: 'Web Multimedia Tutorials',
  artwork: [
    { src: 'https://example.com/artwork-96x96.png', sizes: '96x96', type: 'image/png' },
    { src: 'https://example.com/artwork-512x512.png', sizes: '512x512', type: 'image/png' }
  ]
});

Handling the “Broken State” Problem

The top UX failure I run into: a user closes the PiP window directly, and the main page stays frozen or shows a blank player. The leavepictureinpicture event does fire, but you have to explicitly resume playback if the video was running before the pop-out.

Track the play state right before the transition:

let wasPlaying = false;

video.addEventListener('enterpictureinpicture', () => {
  wasPlaying = !video.paused;
});

video.addEventListener('leavepictureinpicture', () => {
  if (wasPlaying && video.paused) {
    video.play();
  }
  // Scroll the video into view if it's off-screen
  video.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
});

That scrollIntoView call fixes another irritation: the user closes PiP and has no clue where the video went because they scrolled three pages down while it was floating. Gently bringing the video back into the viewport re-establishes context without being pushy.

Smartphone showing a floating video window over a chat application

Mobile Considerations: The Native PiP Bridge

On Android, Chrome uses the same Web API, but behavior can shift. The browser might enter PiP automatically when the user navigates away or the screen turns off—if you’ve set up the Media Session properly. On iOS, Safari supports PiP natively for video elements, but the API still has quirks. For instance, requestPictureInPicture must come from a user gesture, and the video has to be in the viewport (even if not fully visible).

The biggest mobile trap is forgetting how tiny a phone PiP window really is. Controls that felt fine on a 27-inch monitor become pixel-sized targets. Test your Media Session actions on a real device. If you’re building a progressive web app, you might look at the Web App Manifest’s display_override property with "picture-in-picture" to allow a full PiP mode for the whole app—but that’s a different beast for another post.

When PiP Should Not Be Offered

Not every video deserves PiP. A 15-second animated logo loop? Nope. A decorative background video? Don’t tempt people to pop it out. I use a rough rule: if the video has an audio track longer than 60 seconds and the user has actively engaged with it—play, unmute, fullscreen—then PiP makes sense. Otherwise, hide the button.

Also think about the page context. If someone’s in the middle of a multi-step form or a checkout flow, a floating video might yank them away from finishing. You can use the Page Visibility API to notice when they return to the tab and gently suggest bringing the video back if it’s still in PiP. But never force it. Yanking PiP away without the user’s intent is a dark pattern.

Building a Small PiP UX Checklist

Before I ship, I run through a short list that catches most UX headaches:

  • Availability: Is the PiP button hidden when the API isn’t supported?
  • Feedback: Does the button visually reflect the current PiP state?
  • Layout: Does the page hold a usable layout when the video pops out?
  • Controls: Are Media Session actions kept to 3 or fewer, and do they actually work?
  • Resume: Does the video resume playback and scroll back into view when PiP closes?
  • Mobile: Have you tested on a real phone with touch targets?

Each point maps to a moment where a user might get frustrated. Fixing them turns a bare-bones API call into a feature that feels considered.

FAQ

Why does my PiP button work on desktop but not on mobile?

Mobile browsers often demand a user gesture tied directly to the video element. Make sure your button’s click event isn’t hiding inside a delayed callback or a setTimeout. Also check that the video is in the viewport when the user taps—some mobile browsers reject PiP requests if the element sits off-screen.

Can I style the Picture-in-Picture window itself?

Nope. The PiP window is drawn by the operating system or browser. You can’t inject CSS or HTML into it. You can tweak controls and metadata through the Media Session API, but the window’s look—border radius, shadow, size—matches the platform’s design language. Honestly, that’s a UX win: it stays consistent with other PiP windows the user already knows.

How do I handle ads or mid-roll breaks in PiP mode?

This gets thorny. When the video hits an ad break, the PiP window should either pause and show a clear “Ad break” state (if you control the ad insertion) or, if the ad player is separate, exit PiP temporarily. Never trap someone into watching an ad in the floating window with no way to close it. The leavepictureinpicture event can be triggered programmatically if you need to pull the user back to the main page for an interaction.

Implementing PiP without wrecking the UX isn’t about cramming in more code. It’s about choosing the right code that respects how people actually flow through a page. The API is small. The design decisions around it are what make users love the feature—or turn it off.