loke.dev
Header image for The UI That Refused to Be Hidden: How the Document Picture-in-Picture API Liberated My Dashboard

The UI That Refused to Be Hidden: How the Document Picture-in-Picture API Liberated My Dashboard

Stop forcing users to toggle between tabs by breaking your React components out into a persistent, always-on-top floating window.

· 4 min read

The UI That Refused to Be Hidden: How the Document Picture-in-Picture API Liberated My Dashboard

Most web dashboards are data prisons. You spend weeks perfecting a "Mission Control" layout only for your users to bury it under a mountain of Slack windows and Excel spreadsheets the second they actually start working. We’ve been conditioned to think that if a user leaves our tab, our UI might as well not exist. But the Document Picture-in-Picture (PiP) API changes that—it lets you rip a piece of your interface out of the browser and pin it on top of everything else on the user's OS.

This isn't the old Video PiP API that only let you float a boring .mp4. This is the "I can put literally any HTML and React components into an always-on-top window" API. It’s glorious.

Why stop at video?

For years, we’ve used Video PiP for Netflix or YouTube. It’s fine, but it’s limited. If you wanted to add a "Mute" button or a custom progress bar inside that floating window, you were mostly out of luck.

The Document Picture-in-Picture API gives you a blank canvas. I recently used this for a DevOps dashboard. Instead of the user having to alt-tab every five minutes to check if a build finished, I let them "pop out" a small status monitor. It stayed in the corner of their screen, on top of their IDE, showing real-time logs and a "Cancel Build" button.

The Bare Minimum to Get Floating

The API is surprisingly simple, but it has one big rule: it must be triggered by a user gesture (like a click). You can't just spawn windows like it’s 1999.

Here is the basic "Hello World" of Document PiP:

async function openPiP() {
  // 1. Request the window
  const pipWindow = await window.documentPictureInPicture.requestWindow({
    width: 300,
    height: 200,
  });

  // 2. Add some content
  const div = pipWindow.document.createElement('div');
  div.textContent = "I'm floating!";
  pipWindow.document.body.append(div);
}

// Triggered by a button
document.querySelector('#popout-btn').addEventListener('click', openPiP);

Making it Work with React

Writing createElement is for masochists. We want our React components, our state, and our hooks inside that window. The secret sauce here is ReactDOM.createPortal.

Essentially, we open a blank PiP window and then "teleport" a React component into it. However, there’s a catch: CSS doesn't follow you. The PiP window is a completely new document. If you don’t copy your styles over, your beautiful dashboard will look like a Geocities page from 1995.

Here’s a simplified hook-based approach to handling a PiP window in React:

import { useState, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';

const PipWindow = ({ children, onClose }) => {
  const [pipDoc, setPipDoc] = useState(null);
  const pipWindowRef = useRef(null);

  const startPip = async () => {
    const pip = await window.documentPictureInPicture.requestWindow({
      width: 400,
      height: 250,
    });

    // Copy styles from the main document to the PiP window
    [...document.styleSheets].forEach((styleSheet) => {
      try {
        const cssRules = [...styleSheet.cssRules].map((rule) => rule.cssText).join('');
        const style = document.createElement('style');
        style.textContent = cssRules;
        pip.document.head.appendChild(style);
      } catch (e) {
        // Handle cross-origin stylesheet errors
        const link = document.createElement('link');
        link.rel = 'stylesheet';
        link.href = styleSheet.href;
        pip.document.head.appendChild(link);
      }
    });

    pip.addEventListener('pagehide', onClose);
    setPipDoc(pip.document.body);
    pipWindowRef.current = pip;
  };

  useEffect(() => {
    startPip();
    return () => pipWindowRef.current?.close();
  }, []);

  return pipDoc ? createPortal(children, pipDoc) : null;
};

The "Gotchas" that will ruin your day

I spent three hours wondering why my buttons weren't working in the PiP window. It turns out that while state flows perfectly through the portal, event listeners can behave strangely if you're relying on specific window coordinates or global window objects.

1. CSS Frameworks: If you’re using Tailwind or CSS Modules, the style-copying logic above is vital. If you’re using styled-components, you might need to use their StyleSheetManager to target the PiP window's head.
2. The "Close" Logic: When a user closes the PiP window via the browser's 'X' button, the pagehide event fires. You must listen for this to sync your app state, otherwise your main window will think the PiP is still open.
3. Browser Support: Currently, this is a Chromium party (Chrome, Edge). Firefox and Safari are still "considering" it. Always wrap your code in a feature check:
`javascript
if ('documentPictureInPicture' in window) {
// Safe to proceed
}

Real-world Example: The Persistent Timer

Imagine a pomodoro app or a meeting timer. You want the timer visible even when the user is deep in a Google Doc.

function TimerApp() {
  const [isPipOpen, setIsPipOpen] = useState(false);
  const [seconds, setSeconds] = useState(0);

  // Normal timer logic
  useEffect(() => {
    const interval = setInterval(() => setSeconds(s => s + 1), 1000);
    return () => clearInterval(interval);
  }, []);

  return (
    <div className="p-8">
      <h1>Main Dashboard</h1>
      <p>Time elapsed: {seconds}s</p>
      
      <button 
        onClick={() => setIsPipOpen(true)}
        className="bg-blue-500 text-white p-2 rounded"
      >
        Pop Out Timer
      </button>

      {isPipOpen && (
        <PipWindow onClose={() => setIsPipOpen(false)}>
          <div className="pip-container">
            <h3>Keep Working!</h3>
            <div className="timer-display">{seconds}s</div>
            <button onClick={() => setSeconds(0)}>Reset</button>
          </div>
        </PipWindow>
      )}
    </div>
  );
}

When should you actually use this?

Don't be the developer who pops out every single menu. It’s annoying. But if your app has passive monitoring (stock tickers, build statuses, server health) or interactive controls that need to persist across tabs (music players, video call controls), this API is a game-changer.

It’s about respecting the user's workflow. Instead of demanding they stay on your tab, you’re giving them a piece of your app to take with them. That’s not just good tech; it’s good UX.

Go forth and liberate your components. Just remember to copy the CSS, or you'll be floating a very ugly box.