UI & Layout
usePictureInPictureApi
About
Hook for managing Picture-in-Picture video functionality. This hook provides a comprehensive interface to the Picture-in-Picture API, allowing you to control when videos enter and exit PiP mode, track the PiP state, and handle PiP window events. Perfect for creating immersive video experiences that can float above other content.
Examples
Basic video with PiP controls
import { usePictureInPictureApi } from "rooks";
import { useRef } from "react";
 
export default function BasicPiPVideo() {
  const videoRef = useRef(null);
  const { 
    isPiPActive, 
    isSupported, 
    error, 
    pipWindow,
    enterPiP, 
    exitPiP, 
    toggle 
  } = usePictureInPictureApi(videoRef);
 
  return (
    <div style={{ padding: "20px", maxWidth: "600px" }}>
      <h3>Picture-in-Picture Video Player</h3>
      
      {!isSupported && (
        <div style={{ 
          background: "#ffebee", 
          color: "#c62828", 
          padding: "10px", 
          borderRadius: "4px",
          marginBottom: "20px"
        }}>
          Picture-in-Picture is not supported in this browser
        </div>
      )}
 
      {error && (
        <div style={{ 
          background: "#ffebee", 
          color: "#c62828", 
          padding: "10px", 
          borderRadius: "4px",
          marginBottom: "20px"
        }}>
          Error: {error.message}
        </div>
      )}
 
      <video
        ref={videoRef}
        width="100%"
        height="300"
        controls
        src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
        style={{ 
          borderRadius: "8px",
          opacity: isPiPActive ? 0.7 : 1,
          transition: "opacity 0.2s"
        }}
      >
        Your browser does not support the video tag.
      </video>
 
      <div style={{ marginTop: "15px", display: "flex", gap: "10px", alignItems: "center" }}>
        <button 
          onClick={toggle} 
          disabled={!isSupported}
          style={{
            padding: "10px 20px",
            backgroundColor: isPiPActive ? "#f44336" : "#2196F3",
            color: "white",
            border: "none",
            borderRadius: "4px",
            cursor: isSupported ? "pointer" : "not-allowed"
          }}
        >
          {isPiPActive ? "Exit Picture-in-Picture" : "Enter Picture-in-Picture"}
        </button>
        
        <div style={{ color: "#666" }}>
          Status: {isPiPActive ? "In PiP Mode" : "Normal Mode"}
        </div>
      </div>
 
      {pipWindow && (
        <div style={{ 
          marginTop: "15px", 
          padding: "10px", 
          background: "#e3f2fd",
          borderRadius: "4px"
        }}>
          <strong>PiP Window Info:</strong>
          <div>Width: {pipWindow.width}px</div>
          <div>Height: {pipWindow.height}px</div>
        </div>
      )}
    </div>
  );
}Video playlist with PiP
import { usePictureInPictureApi } from "rooks";
import { useRef, useState } from "react";
 
export default function PiPVideoPlaylist() {
  const videoRef = useRef(null);
  const { 
    isPiPActive, 
    isSupported, 
    error, 
    enterPiP, 
    exitPiP 
  } = usePictureInPictureApi(videoRef);
 
  const [currentVideoIndex, setCurrentVideoIndex] = useState(0);
  const [videos] = useState([
    {
      id: 1,
      title: "Big Buck Bunny",
      url: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
      thumbnail: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg"
    },
    {
      id: 2,
      title: "Elephant Dream",
      url: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4",
      thumbnail: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/ElephantsDream.jpg"
    },
    {
      id: 3,
      title: "For Bigger Blazes",
      url: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4",
      thumbnail: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/ForBiggerBlazes.jpg"
    }
  ]);
 
  const currentVideo = videos[currentVideoIndex];
 
  const changeVideo = async (index) => {
    if (isPiPActive) {
      await exitPiP();
    }
    setCurrentVideoIndex(index);
  };
 
  const nextVideo = () => {
    const nextIndex = (currentVideoIndex + 1) % videos.length;
    changeVideo(nextIndex);
  };
 
  const previousVideo = () => {
    const prevIndex = currentVideoIndex === 0 ? videos.length - 1 : currentVideoIndex - 1;
    changeVideo(prevIndex);
  };
 
  return (
    <div style={{ padding: "20px", maxWidth: "800px" }}>
      <h3>Video Playlist with Picture-in-Picture</h3>
      
      {error && (
        <div style={{ 
          background: "#ffebee", 
          color: "#c62828", 
          padding: "10px", 
          borderRadius: "4px",
          marginBottom: "20px"
        }}>
          Error: {error.message}
        </div>
      )}
 
      <div style={{ display: "flex", gap: "20px" }}>
        {/* Main Video Player */}
        <div style={{ flex: 2 }}>
          <video
            ref={videoRef}
            key={currentVideo.id}
            width="100%"
            height="300"
            controls
            src={currentVideo.url}
            style={{ 
              borderRadius: "8px",
              opacity: isPiPActive ? 0.7 : 1,
              transition: "opacity 0.2s"
            }}
          >
            Your browser does not support the video tag.
          </video>
 
          <div style={{ marginTop: "10px" }}>
            <h4>{currentVideo.title}</h4>
            <div style={{ display: "flex", gap: "10px", alignItems: "center" }}>
              <button onClick={previousVideo}>⏮️ Previous</button>
              <button onClick={nextVideo}>⏭️ Next</button>
              <button 
                onClick={() => isPiPActive ? exitPiP() : enterPiP()} 
                disabled={!isSupported}
                style={{
                  backgroundColor: isPiPActive ? "#f44336" : "#2196F3",
                  color: "white",
                  border: "none",
                  padding: "8px 16px",
                  borderRadius: "4px"
                }}
              >
                {isPiPActive ? "📺 Exit PiP" : "🖼️ Enter PiP"}
              </button>
            </div>
          </div>
        </div>
 
        {/* Playlist */}
        <div style={{ flex: 1 }}>
          <h4>Playlist</h4>
          <div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
            {videos.map((video, index) => (
              <div
                key={video.id}
                onClick={() => changeVideo(index)}
                style={{
                  display: "flex",
                  alignItems: "center",
                  gap: "10px",
                  padding: "10px",
                  border: currentVideoIndex === index ? "2px solid #2196F3" : "1px solid #ddd",
                  borderRadius: "4px",
                  cursor: "pointer",
                  backgroundColor: currentVideoIndex === index ? "#e3f2fd" : "white"
                }}
              >
                <img 
                  src={video.thumbnail} 
                  alt={video.title}
                  style={{ width: "60px", height: "40px", objectFit: "cover", borderRadius: "2px" }}
                />
                <div>
                  <div style={{ fontWeight: currentVideoIndex === index ? "bold" : "normal" }}>
                    {video.title}
                  </div>
                  {currentVideoIndex === index && isPiPActive && (
                    <div style={{ fontSize: "0.8em", color: "#2196F3" }}>
                      🖼️ Playing in PiP
                    </div>
                  )}
                </div>
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}Advanced PiP with custom controls
import { usePictureInPictureApi } from "rooks";
import { useRef, useState, useEffect } from "react";
 
export default function AdvancedPiPPlayer() {
  const videoRef = useRef(null);
  const { 
    isPiPActive, 
    isSupported, 
    error, 
    pipWindow,
    enterPiP, 
    exitPiP 
  } = usePictureInPictureApi(videoRef);
 
  const [isPlaying, setIsPlaying] = useState(false);
  const [currentTime, setCurrentTime] = useState(0);
  const [duration, setDuration] = useState(0);
  const [volume, setVolume] = useState(1);
 
  // Update video state
  useEffect(() => {
    const video = videoRef.current;
    if (!video) return;
 
    const updateTime = () => setCurrentTime(video.currentTime);
    const updateDuration = () => setDuration(video.duration);
    const updatePlayState = () => setIsPlaying(!video.paused);
 
    video.addEventListener('timeupdate', updateTime);
    video.addEventListener('loadedmetadata', updateDuration);
    video.addEventListener('play', updatePlayState);
    video.addEventListener('pause', updatePlayState);
 
    return () => {
      video.removeEventListener('timeupdate', updateTime);
      video.removeEventListener('loadedmetadata', updateDuration);
      video.removeEventListener('play', updatePlayState);
      video.removeEventListener('pause', updatePlayState);
    };
  }, []);
 
  const playPause = () => {
    const video = videoRef.current;
    if (video.paused) {
      video.play();
    } else {
      video.pause();
    }
  };
 
  const handleSeek = (e) => {
    const video = videoRef.current;
    const rect = e.currentTarget.getBoundingClientRect();
    const pos = (e.clientX - rect.left) / rect.width;
    video.currentTime = pos * duration;
  };
 
  const handleVolumeChange = (e) => {
    const newVolume = parseFloat(e.target.value);
    setVolume(newVolume);
    if (videoRef.current) {
      videoRef.current.volume = newVolume;
    }
  };
 
  const formatTime = (time) => {
    const minutes = Math.floor(time / 60);
    const seconds = Math.floor(time % 60);
    return `${minutes}:${seconds.toString().padStart(2, '0')}`;
  };
 
  const handlePiPToggle = async () => {
    if (isPiPActive) {
      await exitPiP();
    } else {
      await enterPiP();
    }
  };
 
  return (
    <div style={{ padding: "20px", maxWidth: "700px" }}>
      <h3>Advanced Picture-in-Picture Player</h3>
      
      {!isSupported && (
        <div style={{ 
          background: "#ffebee", 
          color: "#c62828", 
          padding: "10px", 
          borderRadius: "4px",
          marginBottom: "20px"
        }}>
          Picture-in-Picture is not supported in this browser
        </div>
      )}
 
      {error && (
        <div style={{ 
          background: "#ffebee", 
          color: "#c62828", 
          padding: "10px", 
          borderRadius: "4px",
          marginBottom: "20px"
        }}>
          Error: {error.message}
        </div>
      )}
 
      <div style={{ position: "relative" }}>
        <video
          ref={videoRef}
          width="100%"
          height="350"
          src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
          style={{ 
            borderRadius: "8px 8px 0 0",
            opacity: isPiPActive ? 0.7 : 1,
            transition: "opacity 0.2s",
            display: "block"
          }}
        />
 
        {/* Custom Controls */}
        <div style={{ 
          background: "#333", 
          color: "white", 
          padding: "15px",
          borderRadius: "0 0 8px 8px"
        }}>
          {/* Progress Bar */}
          <div 
            style={{ 
              width: "100%", 
              height: "6px", 
              background: "#666", 
              borderRadius: "3px",
              marginBottom: "15px",
              cursor: "pointer"
            }}
            onClick={handleSeek}
          >
            <div style={{ 
              width: `${(currentTime / duration) * 100}%`, 
              height: "100%", 
              background: "#2196F3",
              borderRadius: "3px"
            }} />
          </div>
 
          <div style={{ display: "flex", alignItems: "center", gap: "15px" }}>
            {/* Play/Pause */}
            <button 
              onClick={playPause}
              style={{
                background: "none",
                border: "none",
                color: "white",
                fontSize: "20px",
                cursor: "pointer"
              }}
            >
              {isPlaying ? "⏸️" : "▶️"}
            </button>
 
            {/* Time Display */}
            <div style={{ fontSize: "14px", minWidth: "100px" }}>
              {formatTime(currentTime)} / {formatTime(duration)}
            </div>
 
            {/* Volume Control */}
            <div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
              <span>🔊</span>
              <input
                type="range"
                min="0"
                max="1"
                step="0.1"
                value={volume}
                onChange={handleVolumeChange}
                style={{ width: "80px" }}
              />
              <span>{Math.round(volume * 100)}%</span>
            </div>
 
            {/* PiP Toggle */}
            <button 
              onClick={handlePiPToggle}
              disabled={!isSupported}
              style={{
                background: isPiPActive ? "#f44336" : "#2196F3",
                color: "white",
                border: "none",
                padding: "8px 12px",
                borderRadius: "4px",
                cursor: isSupported ? "pointer" : "not-allowed",
                fontSize: "12px"
              }}
            >
              {isPiPActive ? "Exit PiP" : "Enter PiP"}
            </button>
          </div>
 
          {/* PiP Status */}
          {isPiPActive && pipWindow && (
            <div style={{ 
              marginTop: "10px", 
              fontSize: "12px", 
              color: "#ccc",
              display: "flex",
              justifyContent: "space-between"
            }}>
              <span>🖼️ Playing in Picture-in-Picture mode</span>
              <span>PiP Window: {pipWindow.width}×{pipWindow.height}</span>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}Arguments
| Argument | Type | Description | Default | 
|---|---|---|---|
| videoRef | RefObject<HTMLVideoElement> | React ref pointing to the video element | - | 
Returns
Returns an object with the following properties:
| Return value | Type | Description | Default | 
|---|---|---|---|
| isPiPActive | boolean | Whether the video is currently in Picture-in-Picture mode | false | 
| isSupported | boolean | Whether Picture-in-Picture is supported and available | false | 
| error | Error | null | Any error that occurred during PiP operations | null | 
| pipWindow | PictureInPictureWindow | null | The PiP window object (when active) | null | 
| enterPiP | () => Promise<void> | Function to enter Picture-in-Picture mode | - | 
| exitPiP | () => Promise<void> | Function to exit Picture-in-Picture mode | - | 
| toggle | () => Promise<void> | Function to toggle Picture-in-Picture mode | - | 
TypeScript Support
type PictureInPictureApi = {
  isPiPActive: boolean;
  isSupported: boolean;
  error: Error | null;
  pipWindow: PictureInPictureWindow | null;
  enterPiP: () => Promise<void>;
  exitPiP: () => Promise<void>;
  toggle: () => Promise<void>;
};Browser Support
- Chrome: 69+
- Edge: 79+
- Safari: 13.1+
- Firefox: Not supported yet
The hook includes built-in support detection and will gracefully handle unsupported browsers by setting isSupported to false.
Requirements
For Picture-in-Picture to work, the following conditions must be met:
- The video element must have a valid video source
- document.pictureInPictureEnabledmust be- true
- The video element must not have disablePictureInPictureattribute
- The video element must support the requestPictureInPicture()method
Performance Notes
- The hook automatically sets up and cleans up event listeners for PiP state changes
- PiP operations are asynchronous and may fail (errors are captured in the errorstate)
- Only one video can be in Picture-in-Picture mode at a time across the entire browser
- The PiP window maintains its own controls and can be resized by the user