Beyond Video: Portal Any React Component into Picture-in-Picture with usePip

Leader 2 7 53
calendar_today agoschedule2 min read
— Originally published at dev.to

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 isSupported so 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


Originally published on my blog. You can read the alternative breakdown here.


Originally published on my blog. You can read the alternative breakdown here.

🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

How I Built a React Portfolio in 7 Days That Landed ₹1.2L in Freelance Work

Dharanidharan - Feb 9

Beyond the 98.6°F Myth: Defining Personal Baselines in Health Management

Huifer - Feb 2

5 Web Dev Pitfalls That Are Silently Killing Your Projects (With Real Fixes)

Dharanidharan - Mar 3

React Native Quote Audit - USA

kajolshah - Mar 2

SolidJS 2.0 Async Data: A Deep Dive for React Devs

morellodev - Jul 16
chevron_left
2.3k Points62 Badges
51Posts
8Comments
8Connections
An independent and self-motivated engineering enthusiast with an innovative mindset.

Related Jobs

View all jobs →

Commenters (This Week)

5 comments
3 comments
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!