When a playing video scrolls out of view, an IntersectionObserver on its placeholder can dock it into a floating mini-player within the page (reliable, no permissions), or — only in response to a user gesture — hand it to the browser's Picture-in-Picture window; in both cases leave a same-size placeholder behind so the article does not shift.

Problem / Scenario Context

A news site's explainer videos sit at the top of articles. Readers start a video, then scroll down to read along — and lose the video. Product asks for "picture-in-picture when the video leaves the screen", the pattern familiar from news and video sites. The first attempt calls video.requestPictureInPicture() from the observer callback; it throws a NotAllowedError in most browsers because the call was not triggered by a user gesture. The second attempt moves the video element into a fixed-position container, which works but makes the article jump up by the video's height the moment it happens.

There are two different features here — an in-page floating player and the browser's own PiP window — with different rules. The Media Playback Visibility Control topic covers play/pause on visibility; this page covers keeping playback visible.

Mechanics Explanation

In-page mini-player. The video element (or its wrapper) is restyled from its in-flow position to position: fixed in a corner when the placeholder leaves the viewport, and restored when it returns. No permissions are involved; it is just CSS. The observer watches a placeholder wrapper that stays in flow — not the video itself, which moves once docked and would immediately report as visible again.

Browser Picture-in-Picture. HTMLVideoElement.requestPictureInPicture() opens an always-on-top OS-level window, which survives tab switches. Browsers require transient user activation: the call must happen within a short time of a user gesture such as a click. An IntersectionObserver callback is not a user gesture, so the call is rejected. Chrome additionally supports automatic PiP for media sessions on tab switch in some configurations, and Safari has its own heuristics, but none of them are triggered by scrolling within the page.

Layout. Moving an in-flow element out of flow collapses its space. The placeholder must keep the video's aspect ratio (aspect-ratio on the wrapper) so the content below does not move.

Docking the Video When Its Placeholder LeavesA viewport scrolled below an article's video. The placeholder, which keeps the video's size in the flow, has left the top of the viewport. The playing video is docked into a fixed mini-player in the lower part of the viewport. The article text continues without shifting because the placeholder still occupies the space.placeholder — keeps size, now above viewarticle text — does not shiftdocked mini-player, position: fixedSolid blue frame: viewport (root).

Comparison Table: Mini-Player vs Picture-in-Picture API

Aspect In-page mini-player requestPictureInPicture()
Triggered by scroll alone yes no — needs a user gesture
Survives tab switch no yes
Styling control full none (browser window)
Works on iOS Safari yes yes, from a gesture; video element only
Close / return controls yours browser's
Accessibility you must provide browser-provided controls
Best for "keep watching while reading" "watch while doing something else"

Minimal Reproducible Example

TypeScript
const video = document.querySelector<HTMLVideoElement>('video.explainer')!;
new IntersectionObserver(([e]) => {
  if (!e.isIntersecting && !video.paused) video.requestPictureInPicture();   // NotAllowedError
}).observe(video);

Start the video and scroll down: the console shows NotAllowedError: Must be handling a user gesture.

Production-Safe Solution

HTML
<div class="video-slot" style="aspect-ratio: 16 / 9">
  <div class="video-frame">
    <video class="explainer" controls playsinline src="/v/explainer.mp4"></video>
    <button class="undock" hidden aria-label="Return video to article">×</button>
    <button class="pip" hidden>Pop out</button>
  </div>
</div>
CSS
.video-slot { position: relative; }
.video-frame { position: absolute; inset: 0; }
.video-frame.docked {
  position: fixed; inset: auto 1rem 1rem auto; inline-size: min(320px, 45vw);
  aspect-ratio: 16 / 9; z-index: 20; box-shadow: 0 8px 24px rgb(0 0 0 / 0.3);
}
@media (prefers-reduced-motion: no-preference) {
  .video-frame { transition: inset 200ms ease-out, inline-size 200ms ease-out; }
}
TypeScript
export function dockOnScroll(slot: HTMLElement): () => void {
  const frame = slot.querySelector<HTMLElement>('.video-frame')!;
  const video = frame.querySelector('video')!;
  const undock = frame.querySelector<HTMLButtonElement>('.undock')!;
  const pip = frame.querySelector<HTMLButtonElement>('.pip')!;
  let dismissed = false;

  const setDocked = (d: boolean): void => {
    frame.classList.toggle('docked', d);
    undock.hidden = !d;
    pip.hidden = !d || !document.pictureInPictureEnabled;
  };

  const io = new IntersectionObserver(([e]) => {
    const offscreen = !e.isIntersecting && e.boundingClientRect.top < 0;   // scrolled past, not above
    setDocked(offscreen && !video.paused && !dismissed);
    if (e.isIntersecting) dismissed = false;           // back at the slot: allow docking again
  }, { threshold: 0 });

  undock.addEventListener('click', () => { dismissed = true; setDocked(false); video.pause(); });
  // Real PiP only from a click, which provides the required user activation.
  pip.addEventListener('click', () => { void video.requestPictureInPicture().then(() => setDocked(false)); });
  video.addEventListener('pause', () => setDocked(false));

  io.observe(slot);
  return () => io.disconnect();
}

The observer watches the in-flow slot, which never moves, so docking does not feed back into the observation. The slot's aspect-ratio keeps the article from shifting. Docking happens only when the reader has scrolled past the video (top < 0) while it is playing, never for a paused video, and never again after the reader dismisses it — until they scroll back to it. The Pop out button gives an explicit gesture for browser PiP.

Docking Logic, Step by StepFive steps. The observer watches the in-flow slot, not the video. When the slot leaves the top of the viewport and the video is playing, the frame docks into the corner. The slot keeps its aspect ratio so the article does not shift. The reader can dismiss the mini-player, which pauses it and prevents re-docking until they return to the slot. A Pop out button calls the Picture-in-Picture API from a real click.1Observe the slotThe in-flow wrapper never moves, so no feedback.2Scrolled past + playingDock the frame into the corner.3Slot keeps its sizeaspect-ratio prevents layout shift.4DismissPause and stop re-docking until the reader returns.5Pop outrequestPictureInPicture from a real click.

When Not to Dock

A floating player is intrusive, and some cases call for restraint:

  • Paused videos should never dock; the reader chose to stop.
  • Muted autoplaying loops (background or decorative video) should pause when out of view instead, as in pausing background video to save battery.
  • Small screens have little room; on narrow viewports a docked player can cover the text the reader scrolled down to read. Consider docking only above a minimum viewport width, or docking as a slim bar with audio controls.
  • Reduced motion — keep the dock but drop the animated transition.
  • Screen-reader users need the dock's controls in a sensible reading order and a clear accessible name for the close button; the docked frame should not steal focus when it appears.

Should the Video Dock?A decision chain. If the video is paused, do not dock. Otherwise, if it is a muted decorative loop, pause it instead. Otherwise, if the reader dismissed the mini-player, do not dock until they return to the slot. Otherwise, if the viewport is very narrow, use a slim audio bar or skip docking. Otherwise dock into the corner.Is the video paused?Do not dockyesnoIs it a muted decorative loop?Pause it off-screen insteadyesnoDid the reader dismiss the mini-player?Stay undocked until they returnyesnoIs the viewport very narrow?Slim bar, or no dockyesnoDock into the corner.

Verification Steps

  • Play and scroll past the video; it should dock, and the article should not shift (check Layout Shift regions).
  • Scroll back to the slot; the video returns in place.
  • Pause while docked; the mini-player should close.
  • Dismiss, scroll away and back to confirm dismissal is remembered until return.
  • Click Pop out in Chrome, Safari and Firefox (desktop) and confirm browser PiP opens.

Common Mistakes to Avoid

  • Calling requestPictureInPicture() from the observer. It needs a user gesture.
  • Observing the video element itself. Once docked it is always visible, so the observer never undocks it.
  • Leaving no placeholder. The article jumps by the video's height.
  • Docking paused or decorative video. It adds clutter without value.

FAQ

Can I trigger Picture-in-Picture automatically on scroll?

No. Browsers require transient user activation for requestPictureInPicture, and an IntersectionObserver callback does not provide it. Use an in-page mini-player for scroll-triggered behaviour and offer browser PiP from a button.

Why observe a placeholder rather than the video?

Because the docked video is fixed on screen and always visible. Observing it would report visible as soon as it docks, undocking it again. The placeholder stays in the flow and reflects the article position.

Does moving the video element interrupt playback?

Restyling it with CSS does not. Moving the element to a different parent in the DOM can pause or reload it in some browsers, which is why the frame is restyled in place rather than reparented.

What about iframes like YouTube embeds?

The same docking technique works on the iframe's wrapper. Browser Picture-in-Picture for iframe players depends on the embedded player's own support, which you cannot trigger from the parent page.

Is the Document Picture-in-Picture API an option?

In Chromium-based browsers, documentPictureInPicture.requestWindow can place arbitrary HTML — custom controls, captions — in an always-on-top window, again only from a user gesture. It suits rich players but is not available everywhere.

How do I keep captions working in the mini-player?

Native track elements keep rendering when the video is restyled. Custom caption overlays must be inside the docked frame so they move with it.


↑ Back to Media Playback Visibility Control