Project: pip-it-up
Why is Picture-in-Picture only for video? Bringing native PiP to React components with pip-it-up
We all love Video Picture-in-Picture. Whether it's watching a live stream on the side or catching up on a tutorial while you work, floating windows are incredibly useful. But it begs a simple question: why should video have all the fun?
Why can't we have native PiP for individual UI components?
As a developer who prefers the clean focus of a single-screen setup, I was tired of tab-switching. I wanted a way to tear away just a small, specific part of my web application a markdown editor, a task list, a diagnostic log viewer and stick it always-on-top of my IDE without losing context or breaking my flow.
This is the story of how I built pip-it-up: an open-source React toolkit for the browser's Document Picture-in-Picture API.
The Document PiP API: more powerful than you think
Most developers are familiar with the standard Video Picture-in-Picture API. It lets you pop a <video> element into a small floating window that stays on top of everything else on your screen. Useful, but limited it only works with video.
The Document Picture-in-Picture API is a different beast entirely. Instead of floating a video element, it opens a fully functional floating browser window that can contain any interactive HTML content. Buttons, inputs, iframes, canvas elements, React components all of it works inside a real, always-on-top floating window.
// The raw browser API
const pipWindow = await window.documentPictureInPicture.requestWindow({
width: 400,
height: 300,
});
// Move any DOM node into it
pipWindow.document.body.appendChild(myElement);
On paper this is exactly what we need. In practice, using it directly in a React application is where things get painful.
The three engineering roadblocks
1. CSS stylesheet isolation
When you open a new PiP window, you get a completely blank document. Your Tailwind utility classes? Gone. Your Emotion or Styled Components rules? Gone. Your CSS Modules? Gone.
The PiP window has its own document object with its own <head>. None of the stylesheets from your main application are inherited, which means the moment you move a component into the PiP window, it renders as completely unstyled raw HTML.
This isn't a minor visual glitch, it completely breaks the component.
2. DOM moving vs. virtual DOM state
Here's where things get subtle. When you physically move a DOM node from one document into another (which is what the raw API requires), the browser treats it as a brand new element. For vanilla JavaScript this is manageable, but for React it's a disaster.
React's reconciler tracks component identity through the virtual DOM tree. When you physically reparent a DOM node into a new document, React loses track of it. The result: the component unmounts and remounts, which wipes all internal state, breaks event listeners, resets scroll positions, resets input cursor positions, and kills any active audio or animation state.
If you had a text input with a cursor halfway through a word, it's gone. If you had a Monaco editor open with unsaved changes, it's gone.
3. Browser support
The Document PiP API is currently supported in Chrome 116+ and Edge 116+. Firefox and Safari do not support it at all. Without a fallback strategy, your application simply breaks for a significant chunk of your users.
How pip-it-up solves all three
Real-time style syncing
Instead of hoping styles somehow transfer, pip-it-up uses a MutationObserver to watch your main document's <head> for any <style> or <link rel="stylesheet"> additions or changes, then dynamically clones and mirrors them into the PiP window's document in real time.
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.tagName === 'STYLE' || node.tagName === 'LINK') {
pipWindow.document.head.appendChild(node.cloneNode(true));
}
}
}
});
observer.observe(document.head, { childList: true, subtree: true });
This means Tailwind utilities, CSS-in-JS injected styles, and CSS Modules all work perfectly inside the floating window. The sync is live, if a new stylesheet is injected after the PiP window opens, it gets mirrored automatically.
Virtual tree retention via React Portals
This is the core architectural insight of pip-it-up. Instead of destroying and recreating the React component in the new window, we use a React Portal to physically move the DOM node into the PiP window while keeping the virtual component instance exactly where it was in the tree.
The component never unmounts. React never loses track of it. All context, all state, all event handlers, all cursor positions remain perfectly intact. The component just happens to be rendering its DOM into a different document.
// Simplified version of how Portal Mode works
function PipPortal({ pipWindow, children }) {
return pipWindow
? createPortal(children, pipWindow.document.body)
: children;
}
From React's perspective nothing changed. From the user's perspective, the component is now floating on top of their IDE.
Smart auto-sizing with ResizeObserver
The raw Document PiP API requires you to specify explicit width and height values when opening the window. This means you'd normally need to hardcode dimensions or manually pass them as props — a poor developer experience.
pip-it-up attaches a ResizeObserver to the component being floated and automatically measures its natural dimensions before opening the PiP window, then keeps the window dimensions in sync as the component resizes.
const resizeObserver = new ResizeObserver(([entry]) => {
const { width, height } = entry.contentRect;
// Sync PiP window dimensions automatically
pipWindow.resizeTo(width, height);
});
resizeObserver.observe(componentRef.current);
No width or height props required. It just works.
Graceful fallbacks for Firefox and Safari
When the Document PiP API isn't available, pip-it-up automatically detects this and falls back to a customizable popup, modal, or inline UI whatever you configure. Your users on Firefox and Safari never see a broken page; they just get a slightly different (but fully functional) experience.
Using it in your project
Installation is straightforward:
npm i @pip-it-up/react
Basic usage with PipWrapper and PipTrigger
import { PipWrapper, PipTrigger } from '@pip-it-up/react';
function App() {
return (
<PipWrapper>
<PipTrigger>
<button>Float this widget</button>
</PipTrigger>
<div className="my-widget">
<h2>Reference Checklist</h2>
<ul>
<li>Review PR comments</li>
<li>Run test suite</li>
<li>Update changelog</li>
</ul>
</div>
</PipWrapper>
);
}
When the user clicks the trigger, the widget tears away into a native always-on-top floating window. Click it again and it snaps back with all its state perfectly preserved.
Floating a Monaco editor with state preservation
import { PipWrapper, PipTrigger } from '@pip-it-up/react';
import Editor from '@monaco-editor/react';
function CodePanel() {
const [code, setCode] = useState('// Start coding...');
return (
<PipWrapper>
<PipTrigger>
<button>Pop out editor</button>
</PipTrigger>
<Editor
height="400px"
defaultLanguage="javascript"
value={code}
onChange={setCode}
/>
</PipWrapper>
);
}
The editor floats into a native PiP window. Your cursor position, selection state, scroll position, and the code itself all survive the transition perfectly. You can type in the floating editor while your main application is focused in another window.
What you can build with this
A few ideas to get you thinking:
- Floating reference panels: Keep a keyboard shortcut cheatsheet or API reference visible while you work in your IDE
- Tear-away chat widgets: Pop a support chat or team messaging widget out of a SaaS tool and keep it on-screen
- Floating code playgrounds: Let users experiment with code in a persistent floating window
- Always-on-top dashboards: Pin a metrics panel or log viewer on top of your development environment
- Persistent note-taking: Float a scratchpad widget that stays accessible regardless of what you're focused on
Try it
The live playground has interactive demos including a floating Monaco editor, a Tailwind layout widget, and a keyboard shortcut manager all showing real state preservation in action.
- Live Playground: pip-it-up.vercel.app
- GitHub (MIT): github.com/Shakya47/pip-it-up
-
NPM:
npm i @pip-it-up/react
If you prefer a focused, single-screen workflow, I hope this helps you stay in the zone. And if you build something interesting with it, I'd genuinely love to hear about it in the comments.
What's the first component you'd float?
Top comments (0)