A hover-reveal PiP button on every HTML5 video — including Shadow DOM players like Reddit. Auto-PiP on scroll for YouTube. Works on every site, every browser.
Hover any video → button appears top-right
Features
Most browsers have a built-in PiP button buried in right-click menus. PiP Button puts it front and center — on hover — for every video on every site.
A 34×34px PiP button fades in at the top-right corner of any HTML5 video when you hover over it — and disappears cleanly when you move away. No UI clutter.
Recursively walks Shadow DOM trees to find videos hidden inside web components — including Reddit's video player. Works where other PiP extensions fail entirely.
On YouTube watch pages, the video automatically enters PiP when you scroll it out of view — and exits PiP when you scroll back. Seamless multitasking with zero clicks.
Uses requestPictureInPicture() for Chromium-based browsers and webkitSetPresentationMode for WebKit/Safari — automatically picking the right API per browser.
A throttled MutationObserver watches the DOM for newly added videos — so videos loaded after the initial page render (infinite scroll feeds, SPAs) get a button too.
Pointer event capture ensures your click on the PiP button is never hijacked by the video player beneath it — even on YouTube's notoriously aggressive event-swallowing controls.
How It Works
The extension injects a content script at document_idle that scans the page for HTML5 video elements,
attaches a fixed-position button to each one, and keeps everything in sync as the page changes.
Scans the full DOM including Shadow roots
On load, the script recursively walks every Shadow DOM tree to locate all <video> elements — even those hidden inside closed web components.
Attaches a fixed-position button per video
A position: fixed button is appended directly to document.body for each video. Its position is recalculated on scroll, resize, and layout changes via ResizeObserver and requestAnimationFrame.
Show/hide via global mousemove
Rather than relying on mouseenter (which can be swallowed by Shadow DOM), the script tracks the global cursor position and checks if it falls within the video's bounding rect each frame.
Clicks captured at the window level
All pointer, click, and mousedown events are intercepted at the capture phase before they reach the video player — ensuring the PiP toggle always fires even on heavily instrumented players.
Self-cleaning on video removal
A MutationObserver watches the video's ancestor for DOM changes. If the video is removed (e.g., navigating away in an SPA), its button is immediately removed and its observer is disconnected.
Shadow DOM Traversal
The findAllVideos() function recursively walks any node, entering shadow roots via el.shadowRoot. A Set guards against circular traversal. New shadow roots opened after load are captured by patching Element.prototype.attachShadow.
YouTube Integration
On /watch pages, an IntersectionObserver (50% threshold) automatically triggers PiP entry when the player scrolls off-screen. It wires to YouTube's native .ytp-pip-button in the controls bar and survives YouTube's SPA navigation via URL change detection.
Position Sync
The button's left and top CSS values are recalculated each animation frame when the user scrolls or resizes the window. Off-screen videos have their button hidden via visibility: hidden rather than removed, to avoid repeated DOM mutations.
API Fallback Chain
PiP entry tries webkitSetPresentationMode('picture-in-picture') first (Safari/WebKit), then falls back to video.requestPictureInPicture() (all Chromium browsers). Both paths are guarded against paused videos, which cannot enter PiP.
Browser Compatibility
PiP Button handles the API differences across every major browser so you don't have to think about it.
Videos with disablePictureInPicture set by the site owner are intentionally skipped. Videos smaller than 100×60px are also skipped as they are unlikely to be primary content.
FAQ
Videos inside Shadow DOM trees are isolated from the main document's styling. Injecting a button as a child of the video (or its shadow root) is often impossible or unreliable. Instead, the button is appended to document.body at position: fixed and its coordinates are continuously synced to the video's bounding rect. This approach works universally across all sites and DOM structures.
Yes. Reddit's video player renders inside a Shadow DOM. The extension patches Element.prototype.attachShadow to intercept new shadow roots as they're created, then attaches a MutationObserver inside each one to detect video elements as they load.
On YouTube /watch pages, the extension attaches an IntersectionObserver to the main video element with a 50% visibility threshold. When the video scrolls more than 50% off-screen and is playing, PiP is triggered automatically. When you scroll back and the video is at least 50% visible again, PiP exits and playback resumes inline. YouTube's SPA navigation is also monitored — the observer resets cleanly when you navigate to a new video.
The button is always rendered on hover regardless of playback state — but clicking it on a paused video has no effect. The PiP specification requires a video to be actively playing before it can enter Picture-in-Picture mode. This is a browser API restriction, not a limitation of the extension.
The DOM mutation scan is throttled — new video detection runs at most once every 600ms. Position syncing uses requestAnimationFrame to batch updates. mousemove tracking is marked passive so it never blocks scrolling. The extension is designed to be imperceptible in profiler traces on pages with heavy video content.
No. PiP Button makes zero network requests, stores nothing, and has no analytics or telemetry of any kind. It is a pure content script with no background persistence and no external communication.
Version History
Shadow DOM patch & YouTube SPA stability
Element.prototype.attachShadow patched to intercept late-created shadow roots
YouTube observer now cleanly disconnects and resets on SPA navigation between watch pages
Fixed edge case where button persisted after video element was removed from a dynamic feed
Blocked pointers tracked per pointerId to prevent duplicate click events on multi-touch
Button visibility now uses visibility: hidden (not display: none) for off-screen videos to reduce layout thrashing
YouTube Auto-PiP with IntersectionObserver
Added scroll-triggered auto-PiP for YouTube watch pages at 50% threshold
YouTube native PiP button wired and repositioned to right controls bar
Survival observer keeps YouTube button visible through player DOM re-renders
Fixed-position button architecture
Switched from in-player button injection to position: fixed overlay on document.body
Global mousemove tracking replaces unreliable mouseenter on shadow DOM elements
ResizeObserver added for accurate position sync on fluid layout changes
Initial userscript release
Basic PiP button injected on HTML5 video hover
Dual API support: W3C requestPictureInPicture + WebKit webkitSetPresentationMode
Published to Greasy Fork as a userscript
Free Extension
Install PiP Button and hover any video to get an instant Picture-in-Picture toggle. No setup, no sign-in, no fuss.