npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pip-it-up/core

v0.1.10

Published

Vanilla JS engine for the Document Picture-in-Picture API with automatic video PiP fallback — move, clone, or portal any DOM element into a floating PiP window (public beta)

Readme

@pip-it-up/core

The framework-agnostic JavaScript library for the Document Picture-in-Picture API.

@pip-it-up/core provides a robust, framework-agnostic way to manage the lifecycle of Picture-in-Picture windows, including style synchronization, element positioning, and keyboard event bridging.

Installation

npm install @pip-it-up/core

Usage

<!-- HTML Structure: contentEl lives inside originEl -->
<div id="my-origin">
  <div id="my-content">
    <p>This is the actual element that will move to Picture-in-Picture.</p>
  </div>
</div>
import { createPip } from '@pip-it-up/core';

const contentEl = document.getElementById('my-content');
const originEl = document.getElementById('my-origin');

const pip = createPip({
  mode: 'move', // 'move', 'clone', or 'portal'
  copyStyles: 'sync', // 'sync', 'once', or false
  fallback: 'new-tab' // 'new-tab' or 'none'
});

// Elements are passed to the open call
pip.open({ contentEl, originEl }).then(() => {
  console.log('Picture-in-Picture window opened!');
});

Understanding contentEl vs originEl

| Element Parameter | DOM Role | What to put here | | :--- | :--- | :--- | | contentEl | The Movable Content | The actual UI element/widget you want to display inside the PiP window (e.g. your <video>, interactive editor, chat box, canvas, etc.). | | originEl | The Layout Anchor | The outer parent wrapper element that remains in the main tab. The library uses this element to measure and reserve the layout space (keeping a blank spot of the same dimensions) and automatically re-appends contentEl back here when PiP closes. |

API

createPip(options: PipOptions): PipInstance

Creates a new Picture-in-Picture instance.

PipOptions

Core Options
  • mode: 'move' (default), 'clone', or 'portal'.
  • copyStyles: 'sync' (default), 'once', or false.
  • fallback: 'new-tab' (default) or 'none'.
  • fallbackUrl: The URL to open in a new browser tab/popup when using fallback: 'new-tab'. Required if 'new-tab' is used.
  • width / height: Initial dimensions. If not provided, they are inferred from the element passed to open().
  • fixedSize: Enforces fixed dimensions on the inner document/body styles with overflow: hidden to prevent component layout reflowing. (Note: Snapping the outer OS window frame programmatically is often blocked by modern browser security policies, which restrict resizeTo() calls to active user-gesture contexts).
  • reserveSpace: Preserve the layout in the main window when mode: 'move' (default: true).
  • centerInPip: Centering the content inside the window via flexbox (default: false).
  • pipBodyStyles: Custom styles for the PiP window's <body>.
  • disableVideoPip: Boolean to disable automatic video-only PiP fallback on unsupported browsers (default: false).
Support and Video PiP Utilities

The library exports several helper functions for feature detection and classic Video PiP control:

  • isSupported(): Returns true if the browser supports the Document Picture-in-Picture API.
  • isVideoPipSupported(): Returns true if the browser supports the classic Video Picture-in-Picture API.
  • isWebkitPipSupported(): Returns true if the browser supports WebKit-specific Picture-in-Picture (older Safari).
  • isInVideoPip(): Returns true if any <video> element on the page is currently in Picture-in-Picture.
  • enterVideoPip(video: HTMLVideoElement): Request classic Video PiP on a video element, automatically setting playsinline and using WebKit presentation/fullscreen fallbacks if standard API is missing.
  • exitVideoPip(video: HTMLVideoElement): Exits classic Video PiP mode on the video element.
Advanced Options
  • id: A unique string identifier. If provided, registers the instance globally so it can be retrieved via getPip(id).
  • preferInitialWindowPlacement: Tells the browser to place the PiP window at its default initial position rather than reusing the last position of a previously closed window.
  • disallowReturnToOpener: Hides the browser's native "Return to Tab" button in the PiP window frame.
  • forceFallback: Forces the library to trigger its fallback behavior even if the browser natively supports the Document PiP API (excellent for testing or fallback-by-default behavior).
  • forwardKeyboardEvents: Bubbles keydown and keyup events from the PiP window back to the main opener window so global keyboard shortcuts continue working (default: true).
  • restoreScroll: Automatically snapshots and restores the exact scroll positions of all elements within the moved container upon closing (default: true).
  • restoreFocus: Automatically captures and restores active focus and text/input selections when returning elements to the opener window (default: true).
Lifecycle Callbacks
  • onBeforeOpen: A lifecycle function executed before opening the window. Returning false (or resolving to false) cancels the open request.
  • onOpen: Fired immediately when the window opens, passing the native PiP Window object.
  • onPipWindowReady: Callback fired when the window is fully prepared and first animation frame resolves.
  • onClose: Fired when the PiP window closes.
  • onError: Fired when an error occurs during PiP operations. If omitted, errors are thrown.

PipInstance

  • open({ contentEl?, originEl? }): Requests and opens the Picture-in-Picture window.
    • contentEl (HTMLElement): The actual component or element (e.g. video, textarea, interactive widget) that you want to move into the PiP window.
    • originEl (HTMLElement): The original parent wrapper element in the main tab. When mode: 'move' is used, the library uses this element to measure and preserve the layout space on the main page, and as the return target where contentEl will be automatically re-appended when the PiP window is closed.
  • close(): Closes the window.
  • toggle({ contentEl?, originEl? }): Toggles the window state between open and closed.
  • isOpen(): Returns boolean.
  • getPipWindow(): Returns the Window object or null.
  • getState(): Returns the current state.
  • destroy(): Cleans up listeners and DOM.

Registry API

The library includes a global registry that allows you to share and control Picture-in-Picture instances across different modules of your application (e.g., controlling a single PiP window from separate trigger elements).

[!NOTE] In @pip-it-up/core, registration is fully manual/opt-in. In @pip-it-up/react, the <PipWrapper id="..."> handles registration and cleanup automatically on mount and unmount.

registerPip(id: string, instance: PipInstance): void

Registers a Picture-in-Picture instance in the global registry under a unique string identifier.

unregisterPip(id: string): void

Unregisters a Picture-in-Picture instance by ID from the global registry (essential for cleaning up references and preventing memory leaks).

getPip(id: string): PipInstance | null

Retrieves a registered Picture-in-Picture instance by ID from the global registry. Returns null if no instance is found.

Practical Use Cases

1. Decoupled Triggers

Control a Picture-in-Picture window from a button located anywhere else (e.g., in a global navigation bar):

import { getPip } from '@pip-it-up/core';

// In a completely separate navbar component:
button.addEventListener('click', () => {
  getPip('main-video')?.toggle();
});

2. Global Keyboard Shortcuts

Toggle your Picture-in-Picture window from a global shortcut anywhere on the page:

import { getPip } from '@pip-it-up/core';

window.addEventListener('keydown', (e) => {
  if (e.altKey && e.key.toLowerCase() === 'p') {
    getPip('main-video')?.toggle();
  }
});

3. Navigation Cleanup

Close the Picture-in-Picture window automatically when a user navigates to a new page:

import { getPip } from '@pip-it-up/core';

router.onBeforeEach((to, from) => {
  getPip('main-video')?.close();
});

Security

Keyboard Event Bridge

Only user-initiated keystrokes are forwarded from the PiP window to the opener. Programmatic dispatchEvent() calls in the PiP window are ignored (filtered via e.isTrusted) to prevent synthetic keystroke escalation from PiP-side scripts.

Fallback URL Validation

When using fallback: 'new-tab', the fallbackUrl option is validated to allow only http: and https: protocols. Dangerous schemes like javascript:, data:, and file: are rejected with a console.warn. All new-tab windows are opened with noopener,noreferrer to prevent reverse tabnabbing.

Tips & Gotchas

Cross-Origin Iframes (YouTube, Vimeo, Maps, etc.)

Cross-origin <iframe> embeds (YouTube, Vimeo, Google Maps, Spotify, etc.) will not work inside the PiP window. When the PiP window opens, the iframe is destroyed and recreated in a new document context with a different (or null) origin, causing the embedded service to reject the request (e.g., YouTube Error 153).

This is a browser platform limitation of the Document Picture-in-Picture API, not a bug in pip-it-up.

  • Workaround: For video content, use a native <video> element with a direct source URL instead of an iframe embed. Note that services like YouTube do not provide direct video file URLs — you'll need self-hosted or direct-URL video sources.

Seamless State Preservation (Video, Audio, Canvas, WebRTC)

When using mode: 'move' (default) or mode: 'portal', @pip-it-up moves the actual DOM element without unmounting it. This means stateful DOM content like <video> (keeps playing from the same timestamp), <audio>, <canvas> (keeps its drawing buffer), and WebRTC MediaStream (remains active without reconnecting) will retain their state and identity perfectly across the window boundary.

Clone Mode vs Move Mode

When using the vanilla createPip({ mode: 'clone' }) API, be aware of cloneNode(true) semantics:

  • Event listeners attached via addEventListener are not cloned — only inline handlers (onclick="...") are copied.
  • Inline event handlers (onclick, onmouseover) are cloned and execute in the PiP window's context. If your content includes user-generated or untrusted HTML with inline handlers, prefer mode: 'move' instead to avoid script-injection risks.
  • Form state (typed text, selected options) is not preserved in the clone — only the initial HTML attribute values are copied.
  • <script> tags are cloned but do not re-execute.

React users: <PipWrapper> always uses portal mode internally regardless of the mode prop. Clone mode is only available via the vanilla createPip() API.

Browser Security & Iframe Restrictions

The Document Picture-in-Picture API is governed by strict browser security policies:

  • Top-Level Context Required: The browser strictly prohibits opening a PiP window from inside a nested <iframe> (attempting this will throw NotAllowedError: Opening a PiP window is only allowed from a top-level browsing context).
  • Online Editors (CodeSandbox, StackBlitz): Because online sandboxes run your live preview inside an iframe, the PiP window will fail. To test or demo your code successfully, you must open the live preview in a new standalone browser window/tab (look for the "Open in New Window" icon in the sandbox's preview panel).
  • Secure Context (HTTPS): The API is only active in secure environments (using https:// or localhost).