Document Picture-in-Picture: Always-on-top UI, powered by real HTML

Code aura flashing out of a computer screen.
The Document Picture-in-Picture API lets web apps open an always-on-top window you can fill with real HTML—ideal for timers, controls, and conferencing UI.

For years, browser Picture-in-Picture (PiP) has been synonymous with video: pop a single <video> element into a small always-on-top window and keep watching while you do something else. Useful—but limiting. What if you want custom controls, multiple streams, a timer, chat, notes, or a compact “control panel” that stays visible while the main app remains uncluttered?

That’s the problem the Document Picture-in-Picture API is designed to solve.

What it is

The Document Picture-in-Picture API lets a web app open an always-on-top window that can be filled with arbitrary HTML content—not just a single video element. Think of it as a small companion window for your app that stays in front, ideal for “keep this visible” experiences like:

This API extends the earlier PiP model by moving from “video-only” to “document-level” content.

How it works

At the center of the API is a DocumentPictureInPicture object exposed on the current page:

Once you have that Window, you can treat it a lot like a same-origin popup opened with window.open()—append DOM, set up event handlers, and manage UI—with some important differences.

What makes the PiP window special

According to the MDN overview, a Document PiP window is:

Those constraints are intentional: this window is meant to be a focused, persistent extension of your current app—not a general-purpose popup.

Styling and layout: the “gotcha” you should expect

A Document PiP window opens essentially as a blank document. In practice, this means that if you move or recreate UI in that window, you’ll likely need to bring styles along (for example, by copying stylesheets or injecting style rules). This is one of the first things developers notice when prototyping: the DOM can move, but CSS doesn’t magically follow unless you handle it.

Handling close behavior cleanly

Users can close the PiP window via the browser UI, and your app should respond gracefully. Because the PiP window is still a Window, you can listen for lifecycle events like pagehide to detect closure and restore UI/state back in the main document. A good mental model is: the PiP window is a temporary “presentation surface” for parts of your app, not a separate app.

CSS support: detecting PiP mode

The API also introduces a CSS media feature value:

This allows you to adjust styles when a document is being shown in PiP mode—handy for tightening spacing, increasing hit-target sizes, or simplifying UI when it’s in a compact always-on-top surface.

				
					<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document PiP API Demo</title>
    <link data-wphbdelayedstyle="style.css" rel="stylesheet" />
    <script type="wphb-delay-type" src="documentPictureInPictureAPI.js" defer></script>
</head>

<body>
    <!-- Header Context -->
    <h1>Document Picture-in-Picture</h1>
    <p>Click "Toggle PiP" to move the timer to an always-on-top window.</p>
    
    <!-- The Container we will move between windows -->
    <div id="player-wrapper">
        <div id="pip-content" class="pip-container">
            <h3>Focus Timer</h3>
            <div class="timer-display" id="timer">00:00</div>
            <div class="controls">
                <button id="toggle-pip-btn" class="btn-primary">Toggle PiP</button>
                <button id="reset-btn" class="btn-secondary">Reset</button>
            </div>
            <div style="margin-top: 10px; font-size: 0.8rem; color: #888;">
                Runs in background
            </div>
        </div>
    </div>
<script type="text/javascript" id="wphb-delayed-styles-js">
			(function () {
				const events = ["keydown", "mousemove", "wheel", "touchmove", "touchstart", "touchend"];
				function wphb_load_delayed_stylesheets() {
					document.querySelectorAll("link[data-wphbdelayedstyle]").forEach(function (element) {
						element.setAttribute("href", element.getAttribute("data-wphbdelayedstyle"));
					}),
						 events.forEach(function (event) {
						  window.removeEventListener(event, wphb_load_delayed_stylesheets, { passive: true });
						});
				}
			   events.forEach(function (event) {
				window.addEventListener(event, wphb_load_delayed_stylesheets, { passive: true });
			  });
			})();
		</script></body>

</html>
				
			
				
					/* Base styles for the page layout */
body {
    font-family: 'Segoe UI', system-ui, sans-serif;
    background-color: #f4f4f9;
    color: #333;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100vh;
    margin: 0;
}

h1 {
    margin-bottom: 0.5rem;
}

p {
    color: #666;
    margin-bottom: 2rem;
}

 /* 
           Styles for the specific component we want to move into PiP.
           NOTE: These styles must be manually copied to the PiP window context 
           via JavaScript, or the content will look unstyled in the popup.
        */
 .pip-container {
     background: white;
     border: 1px solid #ddd;
     border-radius: 12px;
     padding: 20px;
     box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
     width: 300px;
     text-align: center;
     transition: all 0.3s ease;
 }

 .timer-display {
     font-size: 3rem;
     font-weight: 700;
     font-variant-numeric: tabular-nums;
     color: #2563eb;
     margin: 10px 0;
 }

 .controls {
     display: flex;
     gap: 10px;
     justify-content: center;
 }

 button {
     cursor: pointer;
     padding: 8px 16px;
     border-radius: 6px;
     border: none;
     font-weight: 600;
     transition: background 0.2s;
 }

 .btn-primary {
     background-color: #2563eb;
     color: white;
 }

 .btn-primary:hover {
     background-color: #1d4ed8;
 }

 .btn-secondary {
     background-color: #e5e7eb;
     color: #374151;
 }

 .btn-secondary:hover {
     background-color: #d1d5db;
 }

 /* Visual indicator when content is active in PiP */
 .pip-active-placeholder {
     border: 2px dashed #ccc;
     border-radius: 12px;
     width: 300px;
     height: 200px;
     display: flex;
     align-items: center;
     justify-content: center;
     color: #888;
     background: rgba(0, 0, 0, 0.05);
 }
				
			
				
					// DOM References
const pipContent = document.querySelector('#pip-content');
const playerWrapper = document.querySelector('#player-wrapper');
const toggleBtn = document.querySelector('#toggle-pip-btn');
const timerDisplay = document.querySelector('#timer');

// State tracking
let pipWindow = null;
let seconds = 0;

// Simple Timer Logic (To prove interactivity works in PiP)
setInterval(() => {
	seconds++;
	const mins = Math.floor(seconds / 60)
		.toString()
		.padStart(2, '0');
	const secs = (seconds % 60).toString().padStart(2, '0');
	timerDisplay.textContent = `${mins}:${secs}`;
}, 1000);

// --- CORE API LOGIC ---

const togglePictureInPicture = async () => {
	// 1. Feature Detection
	if (!('documentPictureInPicture' in window)) {
		alert('Document Picture-in-Picture API is not supported in this browser.');
		return;
	}

	// 2. If PiP is already open, close it (Toggle behavior)
	if (pipWindow) {
		pipWindow.close();
		return;
	}

	try {
		// 3. Request the PiP Window
		// Note: This creates a blank window. We must populate it.
		pipWindow = await documentPictureInPicture.requestWindow({
			width: 340,
			height: 300
		});

		// 4. Style Handling (CRITICAL STEP)
		// PiP windows start with no CSS. We must copy styles from the main window.
		// We copy all CSSRules from standard stylesheets to the new window.
		[...document.styleSheets].forEach((styleSheet) => {
			try {
				const cssRules = [...styleSheet.cssRules]
					.map((rule) => rule.cssText)
					.join('');
				const style = document.createElement('style');
				style.textContent = cssRules;
				pipWindow.document.head.appendChild(style);
			} catch (e) {
				// Catch CORS errors for external stylesheets if necessary
				console.warn('Could not copy stylesheet:', e);
			}
		});

		// 5. Move the Content
		// We append the existing DOM element to the PiP body.
		// This preserves event listeners (like the Reset button).
		pipWindow.document.body.append(pipContent);

		// Update UI state in main window
		toggleBtn.textContent = 'Close PiP';

		// Show a placeholder in the main window so layout doesn't collapse
		const placeholder = document.createElement('div');
		placeholder.id = 'pip-placeholder';
		placeholder.className = 'pip-active-placeholder';
		placeholder.textContent = 'Timer active in Picture-in-Picture';
		playerWrapper.append(placeholder);

		// 6. Handle Closing (Restoration)
		// When user clicks 'X' on the PiP window, we must reclaim the DOM element.
		pipWindow.addEventListener('pagehide', (event) => {
			// Remove placeholder
			const ph = document.querySelector('#pip-placeholder');
			if (ph) ph.remove();

			// Move content back to main window
			playerWrapper.append(pipContent);

			// Reset State
			toggleBtn.textContent = 'Toggle PiP';
			pipWindow = null;
		});
	} catch (err) {
		console.error('Failed to open PiP window:', err);
	}
};

// Event Listener
toggleBtn.addEventListener('click', togglePictureInPicture);

// Reset Logic (proves JS execution context persists)
document.querySelector('#reset-btn').addEventListener('click', () => {
	seconds = 0;
	timerDisplay.textContent = '00:00';
});

				
			

Why it’s exciting

Document PiP is less about “floating video” and more about floating interface. It gives web apps a first-class way to create a lightweight, always-visible companion window while keeping everything in the same session and origin—opening up new UX patterns that previously required awkward popups, native apps, or browser extensions.

If your web app has any “I wish this could stay visible” element, Document Picture-in-Picture is worth exploring.

Share the Post:

Related Posts

small_c_popup.png

Need help?

Let's have a chat...


Login

Jump Back In!

Here at Webolution Designs, we love to learn. This includes sharing things we have learned with you. 

Register

Begin Your Learning Journey Today!

Come back inside to continue your learning journey.