Picture-in-Picture (PiP) has historically been limited to video elements. If you wanted to build a floating music player, a persistent chat widget, or an interactive pomodoro timer that stayed on top of other windows, you were out of luck.
That changes with the browser's new Document Picture-in-Picture API, which allows opening a floating window containing arbitrary HTML content. However, managing this window's lifecycle, rendering React components inside it, and ensuring your CSS styles copy over correctly is a complex task.
To solve this, the latest update to react-hook-lab introduces the usePip hook. This hook makes managing the Document Picture-in-Picture API completely seamless by handling state, managing portal containers, and cloning your stylesheets automatically.
Why usePip?
-
Declarative Portals: Render any React component inside the floating window using a simple
<Pip>component. - Style Inheritance: Automatically clones all stylesheets (including Tailwind or CSS-in-JS rules) from your main document into the PiP window.
-
Browser Fallback: Gracefully detects support with
isSupportedso you can show fallback UI on unsupported browsers.
Example 1: Creating an Interactive Floating Counter
This simple example shows how easily you can open a PiP window and interact with React state from inside the floating layout.
import React, { useState } from "react";
import { usePip } from "react-hook-lab";
export function FloatingCounter() {
const { isSupported, isOpen, openPip, closePip, Pip } = usePip();
const [count, setCount] = useState(0);
if (!isSupported) {
return <p>Document Picture-in-Picture is not supported in this browser.</p>;
}
return (
<div style={{ padding: "20px", border: "1px solid #ccc", borderRadius: "8px" }}>
<h3>Interactive Counter Demo</h3>
<p>Main Window Count: {count}</p>
<button onClick={() => (isOpen ? closePip() : openPip({ width: 300, height: 250 }))}>
{isOpen ? "Close Floating Window" : "Pop Out Counter"}
</button>
<Pip width={300} height={250}>
<div style={{ padding: "20px", textAlign: "center", fontFamily: "sans-serif" }}>
<h4>Floating Controller</h4>
<p style={{ fontSize: "24px", fontWeight: "bold" }}>{count}</p>
<button onClick={() => setCount((c) => c + 1)} style={{ marginRight: "8px" }}>
Increment
</button>
<button onClick={() => setCount((c) => c - 1)}>
Decrement
</button>
</div>
</Pip>
</div>
);
}
Example 2: Building a Floating Pomodoro Timer
Here is a more practical case study: an interactive Pomodoro timer that users can keep pinned to the corner of their screen while working in other apps.
import React, { useState, useEffect } from "react";
import { usePip } from "react-hook-lab";
export function PomodoroTimer() {
const { isOpen, openPip, closePip, Pip } = usePip();
const [seconds, setSeconds] = useState(1500); // 25 minutes
const [isActive, setIsActive] = useState(false);
useEffect(() => {
let interval: any = null;
if (isActive && seconds > 0) {
interval = setInterval(() => setSeconds((s) => s - 1), 1000);
} else {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [isActive, seconds]);
const formatTime = (totalSecs: number) => {
const mins = Math.floor(totalSecs / 60).toString().padStart(2, "0");
const secs = (totalSecs % 60).toString().padStart(2, "0");
return `${mins}:${secs}`;
};
return (
<div style={{ padding: "20px", maxWidth: "400px", margin: "auto" }}>
<h2>Pomodoro Timer</h2>
<p>Time remaining: {formatTime(seconds)}</p>
<button onClick={() => (isOpen ? closePip() : openPip({ width: 280, height: 220 }))}>
{isOpen ? "Disable Floating mode" : "Go Floating"}
</button>
<Pip width={280} height={220}>
<div style={{
padding: "16px",
fontFamily: "sans-serif",
background: "#1e293b",
color: "#ffffff",
height: "100%",
boxSizing: "border-box"
}}>
<h3 style={{ margin: "0 0 10px 0" }}>Focus Mode</h3>
<div style={{ fontSize: "36px", margin: "10px 0" }}>{formatTime(seconds)}</div>
<button onClick={() => setIsActive(!isActive)} style={{ padding: "6px 12px", marginRight: "8px" }}>
{isActive ? "Pause" : "Start"}
</button>
<button onClick={() => { setSeconds(1500); setIsActive(false); }} style={{ padding: "6px 12px" }}>
Reset
</button>
</div>
</Pip>
</div>
);
}
How It Works Behind the Scenes
When you call openPip(), the hook requests a new PiP window using window.documentPictureInPicture.requestWindow(). Once created, it dynamically clones every CSS stylesheet registered on the main window into the header of the new window, ensuring consistent typography and style formatting. Finally, using React's createPortal, it renders your custom markup directly inside the external floating window context.
Resources
- GitHub Repository: react-hook-lab
- NPM Package: react-hook-lab
- LinkedIn Profile: Saurav Pandey
Originally published on my blog. You can read the alternative breakdown here.
Top comments (0)