Web APIs

JavaScript Fullscreen API: Complete Immersive Experience Guide

Master the Fullscreen API in JavaScript for creating immersive web experiences. Learn fullscreen controls, events, and cross-browser implementation.

By JavaScriptDoc Team
fullscreenimmersivevideogamesjavascript

JavaScript Fullscreen API: Complete Immersive Experience Guide

The Fullscreen API enables web applications to display content using the entire screen, creating immersive experiences for videos, games, presentations, and interactive applications.

Understanding the Fullscreen API

The Fullscreen API provides methods to enter and exit fullscreen mode, along with events to track fullscreen state changes.

// Check Fullscreen API support
const checkFullscreenSupport = () => {
  const prefixes = ['', 'webkit', 'moz', 'ms'];

  for (const prefix of prefixes) {
    const property = prefix
      ? `${prefix}FullscreenEnabled`
      : 'fullscreenEnabled';
    if (property in document) {
      return true;
    }
  }

  return false;
};

console.log('Fullscreen supported:', checkFullscreenSupport());

// Get vendor-prefixed methods
const getFullscreenAPI = () => {
  const prefixes = ['', 'webkit', 'moz', 'ms'];
  const api = {};

  for (const prefix of prefixes) {
    if (prefix) {
      if (`${prefix}RequestFullscreen` in Element.prototype) {
        api.requestFullscreen = `${prefix}RequestFullscreen`;
        api.exitFullscreen = `${prefix}ExitFullscreen`;
        api.fullscreenElement = `${prefix}FullscreenElement`;
        api.fullscreenEnabled = `${prefix}FullscreenEnabled`;
        api.fullscreenchange = `${prefix}fullscreenchange`;
        api.fullscreenerror = `${prefix}fullscreenerror`;
        break;
      }
    } else {
      if ('requestFullscreen' in Element.prototype) {
        api.requestFullscreen = 'requestFullscreen';
        api.exitFullscreen = 'exitFullscreen';
        api.fullscreenElement = 'fullscreenElement';
        api.fullscreenEnabled = 'fullscreenEnabled';
        api.fullscreenchange = 'fullscreenchange';
        api.fullscreenerror = 'fullscreenerror';
        break;
      }
    }
  }

  return api;
};

const fullscreenAPI = getFullscreenAPI();
console.log('Fullscreen API:', fullscreenAPI);

Basic Fullscreen Operations

Entering and Exiting Fullscreen

class FullscreenManager {
  constructor() {
    this.api = this.getAPI();
    this.isSupported = this.checkSupport();
    this.callbacks = new Map();
    this.initializeListeners();
  }

  // Get cross-browser API
  getAPI() {
    const prefixes = ['', 'webkit', 'moz', 'ms'];

    for (const prefix of prefixes) {
      const testElement = document.createElement('div');
      const methodName = prefix
        ? `${prefix}RequestFullscreen`
        : 'requestFullscreen';

      if (methodName in testElement) {
        return {
          request: methodName,
          exit: prefix ? `${prefix}ExitFullscreen` : 'exitFullscreen',
          element: prefix ? `${prefix}FullscreenElement` : 'fullscreenElement',
          enabled: prefix ? `${prefix}FullscreenEnabled` : 'fullscreenEnabled',
          change: prefix ? `${prefix}fullscreenchange` : 'fullscreenchange',
          error: prefix ? `${prefix}fullscreenerror` : 'fullscreenerror',
        };
      }
    }

    return null;
  }

  // Check support
  checkSupport() {
    return this.api && document[this.api.enabled] !== undefined;
  }

  // Request fullscreen
  async requestFullscreen(element, options = {}) {
    if (!this.isSupported) {
      throw new Error('Fullscreen API is not supported');
    }

    try {
      // Set up pre-fullscreen state
      this.beforeFullscreen(element);

      // Request fullscreen with options
      if (element[this.api.request]) {
        await element[this.api.request](options);
      } else {
        throw new Error('Element does not support fullscreen');
      }

      // Handle post-fullscreen setup
      this.afterFullscreen(element);

      return true;
    } catch (error) {
      console.error('Fullscreen request failed:', error);
      this.handleError(error);
      throw error;
    }
  }

  // Exit fullscreen
  async exitFullscreen() {
    if (!this.isFullscreen()) {
      return false;
    }

    try {
      await document[this.api.exit]();
      return true;
    } catch (error) {
      console.error('Exit fullscreen failed:', error);
      throw error;
    }
  }

  // Toggle fullscreen
  async toggleFullscreen(element) {
    if (this.isFullscreen()) {
      await this.exitFullscreen();
    } else {
      await this.requestFullscreen(element);
    }
  }

  // Check if in fullscreen
  isFullscreen() {
    return document[this.api.element] != null;
  }

  // Get fullscreen element
  getFullscreenElement() {
    return document[this.api.element];
  }

  // Before fullscreen setup
  beforeFullscreen(element) {
    // Store original styles
    element._originalStyles = {
      position: element.style.position,
      width: element.style.width,
      height: element.style.height,
      zIndex: element.style.zIndex,
    };

    // Add fullscreen class
    element.classList.add('fullscreen-active');
  }

  // After fullscreen setup
  afterFullscreen(element) {
    // Focus element for keyboard events
    if (element.tabIndex < 0) {
      element.tabIndex = 0;
    }
    element.focus();
  }

  // Initialize event listeners
  initializeListeners() {
    if (!this.api) return;

    // Fullscreen change event
    document.addEventListener(this.api.change, (event) => {
      this.handleFullscreenChange(event);
    });

    // Fullscreen error event
    document.addEventListener(this.api.error, (event) => {
      this.handleFullscreenError(event);
    });

    // Keyboard shortcuts
    document.addEventListener('keydown', (event) => {
      this.handleKeyboard(event);
    });
  }

  // Handle fullscreen change
  handleFullscreenChange(event) {
    const element = this.getFullscreenElement();
    const isFullscreen = this.isFullscreen();

    // Notify callbacks
    this.notifyCallbacks('change', {
      isFullscreen,
      element,
      event,
    });

    // Clean up on exit
    if (!isFullscreen && event.target._originalStyles) {
      Object.assign(event.target.style, event.target._originalStyles);
      event.target.classList.remove('fullscreen-active');
      delete event.target._originalStyles;
    }
  }

  // Handle fullscreen error
  handleFullscreenError(event) {
    console.error('Fullscreen error:', event);

    this.notifyCallbacks('error', {
      error: event,
      element: event.target,
    });
  }

  // Handle keyboard events
  handleKeyboard(event) {
    if (!this.isFullscreen()) return;

    // ESC key handling (some browsers handle this automatically)
    if (event.key === 'Escape' && this.isFullscreen()) {
      this.exitFullscreen();
    }

    // F11 key handling
    if (event.key === 'F11') {
      event.preventDefault();
      this.exitFullscreen();
    }
  }

  // Subscribe to events
  on(event, callback) {
    if (!this.callbacks.has(event)) {
      this.callbacks.set(event, []);
    }

    this.callbacks.get(event).push(callback);

    return () => {
      const callbacks = this.callbacks.get(event);
      const index = callbacks.indexOf(callback);
      if (index > -1) {
        callbacks.splice(index, 1);
      }
    };
  }

  // Notify callbacks
  notifyCallbacks(event, data) {
    const callbacks = this.callbacks.get(event) || [];
    callbacks.forEach((callback) => callback(data));
  }

  // Handle errors
  handleError(error) {
    const errorMessages = {
      TypeError: 'Invalid element or options provided',
      NotAllowedError: 'Fullscreen request must be initiated by user action',
      NotSupportedError: 'Fullscreen is not supported on this element',
    };

    const message = errorMessages[error.name] || error.message;

    this.notifyCallbacks('error', {
      error: error,
      message: message,
    });
  }
}

// Usage
const fullscreen = new FullscreenManager();

// Listen for fullscreen changes
fullscreen.on('change', ({ isFullscreen, element }) => {
  console.log('Fullscreen:', isFullscreen);
  console.log('Element:', element);
});

// Request fullscreen on button click
document.getElementById('fullscreenBtn').addEventListener('click', async () => {
  const element = document.getElementById('videoPlayer');

  try {
    await fullscreen.requestFullscreen(element);
  } catch (error) {
    console.error('Failed to enter fullscreen:', error);
  }
});

// Toggle fullscreen
document.addEventListener('dblclick', (event) => {
  const element = event.target.closest('.fullscreen-capable');
  if (element) {
    fullscreen.toggleFullscreen(element);
  }
});

Advanced Fullscreen Features

Fullscreen with Controls

class FullscreenController {
  constructor(element, options = {}) {
    this.element = element;
    this.options = {
      showControls: true,
      autoHideControls: true,
      controlsTimeout: 3000,
      showOnHover: true,
      customControls: null,
      ...options,
    };

    this.fullscreen = new FullscreenManager();
    this.controls = null;
    this.hideTimeout = null;
    this.isControlsVisible = false;

    this.init();
  }

  // Initialize controller
  init() {
    this.createControls();
    this.attachEventListeners();
    this.setupKeyboardShortcuts();
  }

  // Create control interface
  createControls() {
    if (this.options.customControls) {
      this.controls = this.options.customControls;
    } else {
      this.controls = this.createDefaultControls();
    }

    // Add controls to element
    this.element.style.position = 'relative';
    this.element.appendChild(this.controls);

    // Initially hide controls
    this.hideControls(false);
  }

  // Create default controls
  createDefaultControls() {
    const controls = document.createElement('div');
    controls.className = 'fullscreen-controls';
    controls.innerHTML = `
      <div class="controls-bar">
        <button class="control-btn play-pause" title="Play/Pause">
          <span class="icon-play">▶</span>
          <span class="icon-pause" style="display:none">❚❚</span>
        </button>
        
        <div class="progress-container">
          <div class="progress-bar">
            <div class="progress-filled"></div>
            <div class="progress-handle"></div>
          </div>
        </div>
        
        <div class="time-display">
          <span class="current-time">0:00</span>
          <span class="separator">/</span>
          <span class="total-time">0:00</span>
        </div>
        
        <button class="control-btn volume" title="Volume">
          <span class="icon-volume">🔊</span>
        </button>
        
        <input type="range" class="volume-slider" min="0" max="100" value="100">
        
        <button class="control-btn fullscreen-toggle" title="Toggle Fullscreen">
          <span class="icon-enter">⛶</span>
          <span class="icon-exit" style="display:none">✕</span>
        </button>
      </div>
    `;

    // Apply styles
    this.applyControlStyles(controls);

    return controls;
  }

  // Apply control styles
  applyControlStyles(controls) {
    const style = document.createElement('style');
    style.textContent = `
      .fullscreen-controls {
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        background: linear-gradient(transparent, rgba(0,0,0,0.7));
        padding: 20px 10px 10px;
        transition: opacity 0.3s, transform 0.3s;
        z-index: 1000;
      }

      .fullscreen-controls.hidden {
        opacity: 0;
        transform: translateY(100%);
        pointer-events: none;
      }

      .controls-bar {
        display: flex;
        align-items: center;
        gap: 10px;
        color: white;
      }

      .control-btn {
        background: none;
        border: none;
        color: white;
        cursor: pointer;
        font-size: 20px;
        padding: 5px 10px;
        transition: transform 0.2s;
      }

      .control-btn:hover {
        transform: scale(1.1);
      }

      .progress-container {
        flex: 1;
        height: 5px;
        background: rgba(255,255,255,0.3);
        border-radius: 5px;
        cursor: pointer;
        position: relative;
      }

      .progress-filled {
        height: 100%;
        background: #fff;
        border-radius: 5px;
        width: 0%;
        transition: width 0.1s;
      }

      .progress-handle {
        position: absolute;
        top: 50%;
        transform: translate(-50%, -50%);
        width: 15px;
        height: 15px;
        background: white;
        border-radius: 50%;
        left: 0%;
        cursor: grab;
        box-shadow: 0 2px 4px rgba(0,0,0,0.3);
      }

      .progress-handle:active {
        cursor: grabbing;
      }

      .time-display {
        font-size: 14px;
        min-width: 100px;
      }

      .volume-slider {
        width: 80px;
        cursor: pointer;
      }

      /* Fullscreen-specific styles */
      .fullscreen-active .fullscreen-controls {
        padding: 30px 20px 20px;
      }

      .fullscreen-active .control-btn {
        font-size: 24px;
      }

      .fullscreen-active .progress-container {
        height: 8px;
      }
    `;

    document.head.appendChild(style);
  }

  // Attach event listeners
  attachEventListeners() {
    // Fullscreen button
    const fullscreenBtn = this.controls.querySelector('.fullscreen-toggle');
    fullscreenBtn?.addEventListener('click', () => {
      this.toggleFullscreen();
    });

    // Mouse movement for auto-hide
    if (this.options.autoHideControls) {
      this.element.addEventListener('mousemove', () => {
        this.showControls();
        this.scheduleHideControls();
      });

      this.element.addEventListener('mouseleave', () => {
        this.hideControls();
      });
    }

    // Show on hover
    if (this.options.showOnHover) {
      this.element.addEventListener('mouseenter', () => {
        this.showControls();
      });
    }

    // Fullscreen change
    this.fullscreen.on('change', ({ isFullscreen }) => {
      this.updateFullscreenButton(isFullscreen);

      if (isFullscreen) {
        this.element.classList.add('in-fullscreen');
      } else {
        this.element.classList.remove('in-fullscreen');
      }
    });

    // Touch events for mobile
    let touchTimeout;
    this.element.addEventListener('touchstart', () => {
      this.showControls();
      clearTimeout(touchTimeout);

      touchTimeout = setTimeout(() => {
        this.hideControls();
      }, this.options.controlsTimeout);
    });
  }

  // Setup keyboard shortcuts
  setupKeyboardShortcuts() {
    this.element.addEventListener('keydown', (event) => {
      if (!this.fullscreen.isFullscreen()) return;

      switch (event.key) {
        case ' ':
        case 'k':
          event.preventDefault();
          this.togglePlayPause();
          break;

        case 'f':
          event.preventDefault();
          this.toggleFullscreen();
          break;

        case 'ArrowLeft':
          event.preventDefault();
          this.seek(-10);
          break;

        case 'ArrowRight':
          event.preventDefault();
          this.seek(10);
          break;

        case 'ArrowUp':
          event.preventDefault();
          this.adjustVolume(10);
          break;

        case 'ArrowDown':
          event.preventDefault();
          this.adjustVolume(-10);
          break;

        case 'm':
          event.preventDefault();
          this.toggleMute();
          break;
      }
    });
  }

  // Toggle fullscreen
  async toggleFullscreen() {
    try {
      await this.fullscreen.toggleFullscreen(this.element);
    } catch (error) {
      console.error('Fullscreen toggle failed:', error);
    }
  }

  // Update fullscreen button
  updateFullscreenButton(isFullscreen) {
    const btn = this.controls.querySelector('.fullscreen-toggle');
    if (!btn) return;

    const enterIcon = btn.querySelector('.icon-enter');
    const exitIcon = btn.querySelector('.icon-exit');

    if (isFullscreen) {
      enterIcon.style.display = 'none';
      exitIcon.style.display = 'inline';
      btn.title = 'Exit Fullscreen';
    } else {
      enterIcon.style.display = 'inline';
      exitIcon.style.display = 'none';
      btn.title = 'Enter Fullscreen';
    }
  }

  // Show controls
  showControls(immediate = false) {
    clearTimeout(this.hideTimeout);

    if (!this.isControlsVisible) {
      this.isControlsVisible = true;

      if (immediate) {
        this.controls.style.transition = 'none';
      }

      this.controls.classList.remove('hidden');

      if (immediate) {
        // Force reflow
        this.controls.offsetHeight;
        this.controls.style.transition = '';
      }
    }
  }

  // Hide controls
  hideControls(animated = true) {
    clearTimeout(this.hideTimeout);

    if (this.isControlsVisible && this.options.showControls) {
      this.isControlsVisible = false;

      if (!animated) {
        this.controls.style.transition = 'none';
      }

      this.controls.classList.add('hidden');

      if (!animated) {
        // Force reflow
        this.controls.offsetHeight;
        this.controls.style.transition = '';
      }
    }
  }

  // Schedule controls hide
  scheduleHideControls() {
    clearTimeout(this.hideTimeout);

    this.hideTimeout = setTimeout(() => {
      if (this.fullscreen.isFullscreen()) {
        this.hideControls();
      }
    }, this.options.controlsTimeout);
  }

  // Media control methods (implement based on element type)
  togglePlayPause() {
    if (this.element.tagName === 'VIDEO' || this.element.tagName === 'AUDIO') {
      if (this.element.paused) {
        this.element.play();
      } else {
        this.element.pause();
      }
    }
  }

  seek(seconds) {
    if (this.element.tagName === 'VIDEO' || this.element.tagName === 'AUDIO') {
      this.element.currentTime += seconds;
    }
  }

  adjustVolume(change) {
    if (this.element.tagName === 'VIDEO' || this.element.tagName === 'AUDIO') {
      const newVolume = Math.max(
        0,
        Math.min(1, this.element.volume + change / 100)
      );
      this.element.volume = newVolume;
    }
  }

  toggleMute() {
    if (this.element.tagName === 'VIDEO' || this.element.tagName === 'AUDIO') {
      this.element.muted = !this.element.muted;
    }
  }
}

// Usage
const videoElement = document.getElementById('myVideo');
const controller = new FullscreenController(videoElement, {
  showControls: true,
  autoHideControls: true,
  controlsTimeout: 3000,
});

// Custom implementation for non-video elements
const presentationElement = document.getElementById('presentation');
const presentationController = new FullscreenController(presentationElement, {
  customControls: createPresentationControls(),
  showOnHover: false,
});

Fullscreen Styling

CSS for Fullscreen Mode

class FullscreenStyleManager {
  constructor() {
    this.styles = new Map();
    this.styleElement = null;
    this.init();
  }

  // Initialize style manager
  init() {
    this.createStyleElement();
    this.addDefaultStyles();
    this.setupPseudoClasses();
  }

  // Create style element
  createStyleElement() {
    this.styleElement = document.createElement('style');
    this.styleElement.id = 'fullscreen-styles';
    document.head.appendChild(this.styleElement);
  }

  // Add default fullscreen styles
  addDefaultStyles() {
    const defaultStyles = `
      /* Fullscreen pseudo-classes */
      :fullscreen {
        background-color: black;
      }

      :-webkit-full-screen {
        background-color: black;
      }

      :-moz-full-screen {
        background-color: black;
      }

      :-ms-fullscreen {
        background-color: black;
      }

      /* Fullscreen element styles */
      .fullscreen-active {
        width: 100% !important;
        height: 100% !important;
        object-fit: contain;
      }

      /* Video in fullscreen */
      video:fullscreen {
        width: 100%;
        height: 100%;
        object-fit: contain;
      }

      video:-webkit-full-screen {
        width: 100%;
        height: 100%;
        object-fit: contain;
      }

      /* Hide scrollbars in fullscreen */
      :fullscreen::-webkit-scrollbar {
        display: none;
      }

      /* Fullscreen backdrop */
      ::backdrop {
        background-color: black;
      }

      ::-webkit-backdrop {
        background-color: black;
      }

      /* Animations */
      .fullscreen-enter {
        animation: fullscreen-enter 0.3s ease-out;
      }

      .fullscreen-exit {
        animation: fullscreen-exit 0.3s ease-out;
      }

      @keyframes fullscreen-enter {
        from {
          transform: scale(0.8);
          opacity: 0.8;
        }
        to {
          transform: scale(1);
          opacity: 1;
        }
      }

      @keyframes fullscreen-exit {
        from {
          transform: scale(1);
          opacity: 1;
        }
        to {
          transform: scale(0.8);
          opacity: 0.8;
        }
      }

      /* Loading indicator */
      .fullscreen-loading {
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        color: white;
        font-size: 24px;
        z-index: 10000;
      }

      /* Error message */
      .fullscreen-error {
        position: fixed;
        top: 20px;
        left: 50%;
        transform: translateX(-50%);
        background: rgba(255, 0, 0, 0.8);
        color: white;
        padding: 10px 20px;
        border-radius: 5px;
        z-index: 10000;
      }
    `;

    this.updateStyles('default', defaultStyles);
  }

  // Setup pseudo-class detection
  setupPseudoClasses() {
    // Monitor fullscreen changes
    const prefixes = ['', 'webkit', 'moz', 'ms'];

    prefixes.forEach((prefix) => {
      const event = prefix ? `${prefix}fullscreenchange` : 'fullscreenchange';

      document.addEventListener(event, () => {
        this.handleFullscreenChange();
      });
    });
  }

  // Handle fullscreen change
  handleFullscreenChange() {
    const isFullscreen = this.isFullscreen();

    if (isFullscreen) {
      document.body.classList.add('has-fullscreen');
    } else {
      document.body.classList.remove('has-fullscreen');
    }
  }

  // Check if in fullscreen
  isFullscreen() {
    return !!(
      document.fullscreenElement ||
      document.webkitFullscreenElement ||
      document.mozFullScreenElement ||
      document.msFullscreenElement
    );
  }

  // Add custom styles
  addStyles(id, styles) {
    this.styles.set(id, styles);
    this.updateStyleElement();
  }

  // Remove styles
  removeStyles(id) {
    this.styles.delete(id);
    this.updateStyleElement();
  }

  // Update styles
  updateStyles(id, styles) {
    this.styles.set(id, styles);
    this.updateStyleElement();
  }

  // Update style element
  updateStyleElement() {
    const allStyles = Array.from(this.styles.values()).join('\n');
    this.styleElement.textContent = allStyles;
  }

  // Create responsive fullscreen styles
  createResponsiveStyles(breakpoints = {}) {
    const defaultBreakpoints = {
      mobile: 640,
      tablet: 1024,
      desktop: 1280,
      ...breakpoints,
    };

    const responsiveStyles = `
      /* Mobile fullscreen adjustments */
      @media (max-width: ${defaultBreakpoints.mobile}px) {
        :fullscreen {
          padding: 0;
        }

        .fullscreen-controls {
          font-size: 14px;
        }

        .control-btn {
          padding: 10px;
        }
      }

      /* Tablet fullscreen adjustments */
      @media (min-width: ${defaultBreakpoints.mobile + 1}px) and (max-width: ${defaultBreakpoints.tablet}px) {
        .fullscreen-controls {
          padding: 15px;
        }
      }

      /* Desktop fullscreen adjustments */
      @media (min-width: ${defaultBreakpoints.desktop}px) {
        .fullscreen-controls {
          max-width: 1200px;
          margin: 0 auto;
        }
      }

      /* Landscape orientation */
      @media (orientation: landscape) and (max-height: 500px) {
        .fullscreen-controls {
          padding: 10px;
        }

        .controls-bar {
          font-size: 12px;
        }
      }
    `;

    this.addStyles('responsive', responsiveStyles);
  }

  // Apply theme
  applyTheme(theme = 'dark') {
    const themes = {
      dark: {
        background: 'black',
        controlsBg: 'rgba(0, 0, 0, 0.7)',
        text: 'white',
        accent: '#007bff',
      },
      light: {
        background: 'white',
        controlsBg: 'rgba(255, 255, 255, 0.9)',
        text: 'black',
        accent: '#0056b3',
      },
      cinema: {
        background: '#000',
        controlsBg: 'rgba(20, 20, 20, 0.9)',
        text: '#ccc',
        accent: '#e50914',
      },
    };

    const selectedTheme = themes[theme] || themes.dark;

    const themeStyles = `
      :fullscreen {
        background-color: ${selectedTheme.background};
      }

      .fullscreen-controls {
        background: linear-gradient(transparent, ${selectedTheme.controlsBg});
        color: ${selectedTheme.text};
      }

      .control-btn {
        color: ${selectedTheme.text};
      }

      .control-btn:hover {
        color: ${selectedTheme.accent};
      }

      .progress-filled {
        background: ${selectedTheme.accent};
      }
    `;

    this.addStyles('theme', themeStyles);
  }
}

// Usage
const styleManager = new FullscreenStyleManager();

// Add custom styles
styleManager.addStyles(
  'custom',
  `
  .my-fullscreen-element:fullscreen {
    display: flex;
    align-items: center;
    justify-content: center;
  }
`
);

// Apply theme
styleManager.applyTheme('cinema');

// Create responsive styles
styleManager.createResponsiveStyles({
  mobile: 480,
  tablet: 768,
  desktop: 1920,
});

Fullscreen Gallery

Image Gallery with Fullscreen

class FullscreenGallery {
  constructor(container, images = []) {
    this.container = container;
    this.images = images;
    this.currentIndex = 0;
    this.fullscreen = new FullscreenManager();
    this.viewer = null;

    this.init();
  }

  // Initialize gallery
  init() {
    this.createGallery();
    this.createViewer();
    this.attachEventListeners();
  }

  // Create gallery grid
  createGallery() {
    this.container.innerHTML = '';
    this.container.className = 'fullscreen-gallery';

    const grid = document.createElement('div');
    grid.className = 'gallery-grid';

    this.images.forEach((image, index) => {
      const item = document.createElement('div');
      item.className = 'gallery-item';
      item.innerHTML = `
        <img src="${image.thumbnail || image.src}" alt="${image.alt || ''}" />
        <div class="gallery-overlay">
          <button class="view-btn" data-index="${index}">
            <span>⛶</span> View
          </button>
        </div>
      `;

      grid.appendChild(item);
    });

    this.container.appendChild(grid);
    this.applyGalleryStyles();
  }

  // Create fullscreen viewer
  createViewer() {
    this.viewer = document.createElement('div');
    this.viewer.className = 'fullscreen-viewer';
    this.viewer.innerHTML = `
      <div class="viewer-container">
        <img class="viewer-image" src="" alt="" />
        <div class="viewer-loading">Loading...</div>
      </div>
      
      <div class="viewer-controls">
        <button class="viewer-btn prev" title="Previous">❮</button>
        <button class="viewer-btn next" title="Next">❯</button>
        <button class="viewer-btn close" title="Close">✕</button>
        
        <div class="viewer-info">
          <span class="image-counter">1 / ${this.images.length}</span>
          <span class="image-title"></span>
        </div>
        
        <div class="viewer-thumbnails">
          ${this.images
            .map(
              (img, i) => `
            <img class="thumbnail ${i === 0 ? 'active' : ''}" 
                 src="${img.thumbnail || img.src}" 
                 data-index="${i}" />
          `
            )
            .join('')}
        </div>
      </div>
    `;

    document.body.appendChild(this.viewer);
    this.applyViewerStyles();
  }

  // Apply gallery styles
  applyGalleryStyles() {
    const style = document.createElement('style');
    style.textContent = `
      .fullscreen-gallery {
        padding: 20px;
      }

      .gallery-grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
        gap: 20px;
      }

      .gallery-item {
        position: relative;
        overflow: hidden;
        border-radius: 8px;
        cursor: pointer;
        aspect-ratio: 1;
      }

      .gallery-item img {
        width: 100%;
        height: 100%;
        object-fit: cover;
        transition: transform 0.3s;
      }

      .gallery-item:hover img {
        transform: scale(1.05);
      }

      .gallery-overlay {
        position: absolute;
        inset: 0;
        background: rgba(0, 0, 0, 0.7);
        display: flex;
        align-items: center;
        justify-content: center;
        opacity: 0;
        transition: opacity 0.3s;
      }

      .gallery-item:hover .gallery-overlay {
        opacity: 1;
      }

      .view-btn {
        background: rgba(255, 255, 255, 0.9);
        border: none;
        padding: 10px 20px;
        border-radius: 5px;
        cursor: pointer;
        font-size: 16px;
        display: flex;
        align-items: center;
        gap: 5px;
      }
    `;

    document.head.appendChild(style);
  }

  // Apply viewer styles
  applyViewerStyles() {
    const style = document.createElement('style');
    style.textContent = `
      .fullscreen-viewer {
        display: none;
        background: black;
      }

      .fullscreen-viewer:fullscreen {
        display: flex;
        flex-direction: column;
      }

      .viewer-container {
        flex: 1;
        display: flex;
        align-items: center;
        justify-content: center;
        position: relative;
        overflow: hidden;
      }

      .viewer-image {
        max-width: 100%;
        max-height: 100%;
        object-fit: contain;
        transition: opacity 0.3s;
      }

      .viewer-image.loading {
        opacity: 0;
      }

      .viewer-loading {
        position: absolute;
        color: white;
        font-size: 20px;
        display: none;
      }

      .viewer-loading.active {
        display: block;
      }

      .viewer-controls {
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
        padding: 20px;
        color: white;
      }

      .viewer-btn {
        position: absolute;
        background: rgba(255, 255, 255, 0.2);
        border: none;
        color: white;
        font-size: 30px;
        padding: 10px 15px;
        cursor: pointer;
        transition: background 0.3s;
      }

      .viewer-btn:hover {
        background: rgba(255, 255, 255, 0.3);
      }

      .viewer-btn.prev {
        left: 20px;
        top: 50%;
        transform: translateY(-50%);
      }

      .viewer-btn.next {
        right: 20px;
        top: 50%;
        transform: translateY(-50%);
      }

      .viewer-btn.close {
        top: 20px;
        right: 20px;
        font-size: 20px;
      }

      .viewer-info {
        text-align: center;
        margin-bottom: 10px;
      }

      .viewer-thumbnails {
        display: flex;
        gap: 10px;
        overflow-x: auto;
        padding: 10px 0;
        justify-content: center;
      }

      .viewer-thumbnails img {
        width: 60px;
        height: 60px;
        object-fit: cover;
        border: 2px solid transparent;
        cursor: pointer;
        opacity: 0.6;
        transition: all 0.3s;
      }

      .viewer-thumbnails img.active {
        border-color: white;
        opacity: 1;
      }

      .viewer-thumbnails img:hover {
        opacity: 1;
      }

      /* Touch gestures */
      .viewer-container.dragging {
        cursor: grabbing;
      }
    `;

    document.head.appendChild(style);
  }

  // Attach event listeners
  attachEventListeners() {
    // Gallery item clicks
    this.container.addEventListener('click', (e) => {
      const viewBtn = e.target.closest('.view-btn');
      if (viewBtn) {
        const index = parseInt(viewBtn.dataset.index);
        this.openViewer(index);
      }
    });

    // Viewer controls
    this.viewer.querySelector('.prev').addEventListener('click', () => {
      this.showPrevious();
    });

    this.viewer.querySelector('.next').addEventListener('click', () => {
      this.showNext();
    });

    this.viewer.querySelector('.close').addEventListener('click', () => {
      this.closeViewer();
    });

    // Thumbnail clicks
    this.viewer.addEventListener('click', (e) => {
      const thumbnail = e.target.closest('.thumbnail');
      if (thumbnail) {
        const index = parseInt(thumbnail.dataset.index);
        this.showImage(index);
      }
    });

    // Keyboard navigation
    document.addEventListener('keydown', (e) => {
      if (!this.fullscreen.isFullscreen()) return;

      switch (e.key) {
        case 'ArrowLeft':
          this.showPrevious();
          break;
        case 'ArrowRight':
          this.showNext();
          break;
        case 'Escape':
          this.closeViewer();
          break;
      }
    });

    // Touch gestures
    this.setupTouchGestures();
  }

  // Setup touch gestures
  setupTouchGestures() {
    let startX = 0;
    let startY = 0;
    let distX = 0;
    let distY = 0;

    const container = this.viewer.querySelector('.viewer-container');

    container.addEventListener('touchstart', (e) => {
      startX = e.touches[0].clientX;
      startY = e.touches[0].clientY;
    });

    container.addEventListener('touchmove', (e) => {
      if (!startX || !startY) return;

      distX = e.touches[0].clientX - startX;
      distY = e.touches[0].clientY - startY;
    });

    container.addEventListener('touchend', () => {
      if (Math.abs(distX) > Math.abs(distY) && Math.abs(distX) > 50) {
        if (distX > 0) {
          this.showPrevious();
        } else {
          this.showNext();
        }
      }

      startX = 0;
      startY = 0;
      distX = 0;
      distY = 0;
    });
  }

  // Open viewer
  async openViewer(index) {
    this.currentIndex = index;

    try {
      await this.fullscreen.requestFullscreen(this.viewer);
      this.viewer.style.display = 'flex';
      this.showImage(index);
    } catch (error) {
      console.error('Failed to open fullscreen viewer:', error);
    }
  }

  // Close viewer
  async closeViewer() {
    try {
      await this.fullscreen.exitFullscreen();
      this.viewer.style.display = 'none';
    } catch (error) {
      console.error('Failed to close viewer:', error);
    }
  }

  // Show image
  showImage(index) {
    if (index < 0 || index >= this.images.length) return;

    this.currentIndex = index;
    const image = this.images[index];
    const viewerImage = this.viewer.querySelector('.viewer-image');
    const loading = this.viewer.querySelector('.viewer-loading');

    // Show loading
    viewerImage.classList.add('loading');
    loading.classList.add('active');

    // Load image
    const img = new Image();
    img.onload = () => {
      viewerImage.src = img.src;
      viewerImage.alt = image.alt || '';
      viewerImage.classList.remove('loading');
      loading.classList.remove('active');
    };

    img.onerror = () => {
      loading.textContent = 'Failed to load image';
    };

    img.src = image.src;

    // Update UI
    this.updateViewerUI();
  }

  // Update viewer UI
  updateViewerUI() {
    // Update counter
    const counter = this.viewer.querySelector('.image-counter');
    counter.textContent = `${this.currentIndex + 1} / ${this.images.length}`;

    // Update title
    const title = this.viewer.querySelector('.image-title');
    const currentImage = this.images[this.currentIndex];
    title.textContent = currentImage.title || '';

    // Update thumbnails
    const thumbnails = this.viewer.querySelectorAll('.thumbnail');
    thumbnails.forEach((thumb, index) => {
      thumb.classList.toggle('active', index === this.currentIndex);
    });

    // Update navigation buttons
    const prevBtn = this.viewer.querySelector('.prev');
    const nextBtn = this.viewer.querySelector('.next');

    prevBtn.style.display = this.currentIndex > 0 ? 'block' : 'none';
    nextBtn.style.display =
      this.currentIndex < this.images.length - 1 ? 'block' : 'none';
  }

  // Show previous image
  showPrevious() {
    if (this.currentIndex > 0) {
      this.showImage(this.currentIndex - 1);
    }
  }

  // Show next image
  showNext() {
    if (this.currentIndex < this.images.length - 1) {
      this.showImage(this.currentIndex + 1);
    }
  }
}

// Usage
const images = [
  {
    src: '/images/photo1-full.jpg',
    thumbnail: '/images/photo1-thumb.jpg',
    title: 'Mountain Landscape',
    alt: 'Beautiful mountain landscape',
  },
  {
    src: '/images/photo2-full.jpg',
    thumbnail: '/images/photo2-thumb.jpg',
    title: 'Ocean Sunset',
    alt: 'Sunset over the ocean',
  },
  // More images...
];

const galleryContainer = document.getElementById('gallery');
const gallery = new FullscreenGallery(galleryContainer, images);

Best Practices

  1. Always check for user interaction

    // Fullscreen must be triggered by user action
    button.addEventListener('click', async () => {
      try {
        await element.requestFullscreen();
      } catch (error) {
        console.error('Fullscreen denied:', error);
      }
    });
    
  2. Handle all vendor prefixes

    const requestFullscreen =
      element.requestFullscreen ||
      element.webkitRequestFullscreen ||
      element.mozRequestFullScreen ||
      element.msRequestFullscreen;
    
  3. Provide exit mechanisms

    // Always show exit button in fullscreen
    const exitButton = createExitButton();
    element.appendChild(exitButton);
    
  4. Test across browsers and devices

    // Different behavior on mobile vs desktop
    if (isMobile()) {
      // Adjust UI for mobile fullscreen
    }
    

Conclusion

The Fullscreen API enables powerful immersive experiences:

  • Video players with cinema mode
  • Image galleries with lightbox effects
  • Games with immersive gameplay
  • Presentations with distraction-free viewing
  • Document viewers with focused reading
  • Interactive apps with full-screen modes

Key takeaways:

  • Always require user interaction
  • Handle all browser prefixes
  • Provide clear exit mechanisms
  • Style appropriately for fullscreen
  • Test across different devices
  • Consider mobile differences

Master the Fullscreen API to create engaging, immersive web experiences!