Hess WebTech

Build It Right. Support It Well.

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

Code aura flashing out of a computer screen.

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 href="style.css" rel="stylesheet" />
    <script 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>
</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.

more posts:
Digital software technology development concept. Coder programmer, software engineer coding computer language, javascript on laptop computer
Code

An overview of the Fullscreen API

The Fullscreen API lets you display a specific element (and its descendants) in true fullscreen and then return to windowed mode. Learn the key methods, state checks, events, permissions considerations, and UX best practices for building focused video, game, and presentation experiences.

Discover More »
Document Management System (DMS) being setup by IT consultant
Code

A quick tour of the Web File API

The File API enables web applications to access files and their contents when the user makes them available—typically via an control or drag and drop. Selected files are exposed as a FileList, which contains File objects that provide metadata like name, size, type, and last modified date. You can read a file’s contents using FileReader (asynchronously) or FileReaderSync in web workers, and you can also work with binary data through Blob objects.

Discover More »