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:
- Custom video players (including multiple videos and custom UI)
- Video conferencing layouts (participant streams + mute/end-call controls)
- Always-visible productivity widgets (timers, to-dos, notes, messenger panels)
- Secondary HUD-like UI for apps and games (controls, instructions, lore)
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:
- Window.documentPictureInPicture gives you access to the API entry point.
- Calling documentPictureInPicture.requestWindow() opens the PiP window and returns a Promise that resolves to the PiP window’s own Window object.
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:
- Always on top of other windows
- Bound to the opening tab (it “never outlives” the opener)
- Not navigable (you can’t redirect it to another URL)
- Not positionable by the site (you can’t choose screen coordinates)
- Limited: typically one per tab (and browsers may impose additional global limits)
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:
- display-mode: picture-in-picture
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.
Document PiP API Demo
Document Picture-in-Picture
Click "Toggle PiP" to move the timer to an always-on-top window.
Focus Timer
00:00
Runs in background
/* 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.

