Tutorials Books Videos Forums

Change the theme! Search!
Rambo ftw!

Customize Theme


Color

Background


Done

Table of Contents

Detecting Browser Zoom Changes in JavaScript

by kirupa   |   filed under JavaScript 101

Whenever our browser resizes, a resize event gets fired. This is your friendly run-of-the-mill DOM event that we can detect and react to using JavaScript. Now, when you and I think about resizing, we probably think of our browser’s dimensions actually changing:

As it turns out, there is another seemingly unrelated activity that also fires a resize event. That activity is zooming:

Here is the question. If we want to specifically only detect a zoom action, how can we do that? In this article, let’s figure out how to do that.

Onwards!

Zooming is Sorta Kinda Like Resizing

Let’s get one thing out of the way up front. When we zoom in or zoom out, there is no dedicated zoom event that fires. I repeat. There is no zoom event:

Instead, what gets fired is a resize event. This sorta makes sense when look into what is happening at the page level. Whenever we zoom in or zoom out, the available width and height of our page changes. That UI elements also get bigger or smaller is a detail that gets clubbed in as merely being a resize activity.

All hope isn’t lost, though. There is a way we can detect a browser zoom. Take a look at the following live example:

As we zoom in and out, the page detects the zoom and updates the current zoom value accordingly. In the following sections, we'll look at the logic behind how we are able to detect a browser (or pinch) zoom. This is going to be a hoot!

Checking the Viewport and Browser Size

When we look at a typical web page, there are two sizes at play:

The two sizes are:

  1. The browser size (referenced by window.outerWidth and window.outerHeight) measures the total size of the browser window in pixels, including all browser UI elements like the window frame, toolbars, scrollbars, etc. This represents the full area of the browser window as you and I see it on our screen.
  2. The viewport size (referenced by window.innerWidth and window.innerHeight ) measures the size of the web page's visible content area in pixels. This is the actual space available for our content, and it does not include the browser's UI elements like scrollbars, toolbars, etc.

When we zoom in or zoom out, the browser size never changes. It remains at whatever dimensions we have originally sized it at. What does change is our viewport size:


When we zoom in, our viewport size shrinks, and things get cramped. When we zoom out, our viewport size gets larger as there is now more space to arrange things.

This relationship between the browser size and viewport size is an important detail that is core to how we implement our zoom detection logic. If we detect a situation where the browser size remains the same but the viewport size changes, we can safely assume a zoom operation has taken place. If we turn all of this explanation into a working example, we’ll have the following:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Detecting Zoom</title>

  <h1 id="zoomText">100%</h1>

  <style>
    body {
      margin: 0;
      padding: 0;
      height: 100vh;
      width: 100vw;

      display: flex;
      justify-content: center;
      align-items: center;

      background-color: #D3FAC7;
    }

    h1 {
      font-family: sans-serif;
      color: #32432c;
      opacity: .7;
    }
  </style>
</head>

<body>
  <script>
    /**
     * ZoomDetector class - Monitors and reports browser zoom level changes
     * Handles both browser zoom (Ctrl+/- or Cmd+/-) and pinch-to-zoom gestures
     */
    class ZoomDetector {
      /**
       * Initialize the detector with initial window measurements
       * Sets up event listeners for zoom detection
       */
      constructor() {
        // Store initial window width for comparison
        this.lastWidth = window.innerWidth;
        // Calculate and store initial zoom level
        this.lastScale = this.getZoomLevel();
        // Set up event listeners for zoom detection
        this.setupListeners();
      }

      /**
       * Calculate the current zoom level as a percentage
       * Uses the ratio of outer window width to inner window width
       * @returns {number} Zoom level as a percentage (e.g., 100 for 100%)
       */
      getZoomLevel() {
        return Math.round(window.outerWidth / window.innerWidth * 100);
      }

      /**
       * Set up event listeners for different types of zoom events
       * Handles both browser zoom and pinch-to-zoom gestures
       */
      setupListeners() {
        // Listen for browser window resize events (triggered by browser zoom)
        window.addEventListener('resize', () => {
          // Use requestAnimationFrame to optimize performance
          // Ensures zoom check happens during the next frame render
          requestAnimationFrame(() => this.checkZoom());
        });

        // Handle pinch-to-zoom events using visualViewport API if available
        if (window.visualViewport) {
          window.visualViewport.addEventListener('resize', () => {
            // Only process if window width hasn't changed
            // This prevents duplicate events when both viewport and window resize
            if (window.innerWidth === this.lastWidth) {
              this.checkZoom();
            }
          });
        }
      }

      /**
       * Check if zoom level has changed and dispatch appropriate events
       * Determines zoom direction and triggers custom zoom event
       */
      checkZoom() {
        // Get current measurements
        const currentScale = this.getZoomLevel();
        const currentWidth = window.innerWidth;

        // Only proceed if there's an actual change in zoom or window size
        if (currentScale !== this.lastScale || currentWidth !== this.lastWidth) {
          // Determine if user is zooming in or out
          const direction = currentScale > this.lastScale ? 'in' : 'out';
          console.log(`Zoom ${direction} detected: ${currentScale}%`);

          // Create and dispatch custom zoom event with detailed information
          window.dispatchEvent(new CustomEvent('zoom', {
            detail: {
              oldScale: this.lastScale,      // Previous zoom level
              newScale: currentScale,         // New zoom level
              direction: direction,           // Zoom direction ('in' or 'out')
              isWindowResize: currentWidth !== this.lastWidth  // Whether window size changed
            }
          }));

          // Update stored values for next comparison
          this.lastScale = currentScale;
          this.lastWidth = currentWidth;
        }
      }
    }

    // Initialize zoom detector and update initial zoom display
    const zoomDetector = new ZoomDetector();
    updateZoomText(zoomDetector.getZoomLevel());

    // Set up listener for custom zoom events
    window.addEventListener('zoom', (e) => {
      // Extract zoom details from event
      const { oldScale, newScale, direction, isWindowResize } = e.detail;
      // Update zoom level display
      updateZoomText(newScale);
    });

    /**
     * Update the zoom level display in the UI
     * @param {number} zoomValue - Current zoom level percentage
     */
    function updateZoomText(zoomValue) {
      document.querySelector("#zoomText").innerText = "🔎 " + zoomValue + "%";
    }
  </script>
</body>

</html>

Create a new document and paste all of the above HTML, CSS, and JavaScript into a new document. Then, preview your changes in the browser. When you load this page and zoom in or zoom out, you’ll see the current zoom value update to reflect the latest zoom value:

This zoom detection example should work identically in Chrome and Safari. That this works in Safari is pretty neat, for a lot of the approaches I saw online don’t support Safari. This example doesn't work consistently in Firefox, so do note this limitation.

Understanding the Code

Before we call it a day, let’s take a look at both how to use our code and understand why it works the way it does.

Using this Code

The way to use this code is to make sure the ZoomDetector class is added to your document:

/**
 * ZoomDetector class - Monitors and reports browser zoom level changes
 * Handles both browser zoom (Ctrl+/- or Cmd+/-) and pinch-to-zoom gestures
 */
class ZoomDetector {
  /**
   * Initialize the detector with initial window measurements
   * Sets up event listeners for zoom detection
   */
  constructor() {
    // Store initial window width for comparison
    this.lastWidth = window.innerWidth;
    // Calculate and store initial zoom level
    this.lastScale = this.getZoomLevel();
    // Set up event listeners for zoom detection
    this.setupListeners();
  }

  /**
   * Calculate the current zoom level as a percentage
   * Uses the ratio of outer window width to inner window width
   * @returns {number} Zoom level as a percentage (e.g., 100 for 100%)
   */
  getZoomLevel() {
    return Math.round(window.outerWidth / window.innerWidth * 100);
  }

  /**
   * Set up event listeners for different types of zoom events
   * Handles both browser zoom and pinch-to-zoom gestures
   */
  setupListeners() {
    // Listen for browser window resize events (triggered by browser zoom)
    window.addEventListener('resize', () => {
      // Use requestAnimationFrame to optimize performance
      // Ensures zoom check happens during the next frame render
      requestAnimationFrame(() => this.checkZoom());
    });

    // Handle pinch-to-zoom events using visualViewport API if available
    if (window.visualViewport) {
      window.visualViewport.addEventListener('resize', () => {
        // Only process if window width hasn't changed
        // This prevents duplicate events when both viewport and window resize
        if (window.innerWidth === this.lastWidth) {
          this.checkZoom();
        }
      });
    }
  }

  /**
   * Check if zoom level has changed and dispatch appropriate events
   * Determines zoom direction and triggers custom zoom event
   */
  checkZoom() {
    // Get current measurements
    const currentScale = this.getZoomLevel();
    const currentWidth = window.innerWidth;

    // Only proceed if there's an actual change in zoom or window size
    if (currentScale !== this.lastScale || currentWidth !== this.lastWidth) {
      // Determine if user is zooming in or out
      const direction = currentScale > this.lastScale ? 'in' : 'out';
      console.log(`Zoom ${direction} detected: ${currentScale}%`);

      // Create and dispatch custom zoom event with detailed information
      window.dispatchEvent(new CustomEvent('zoom', {
        detail: {
          oldScale: this.lastScale,      // Previous zoom level
          newScale: currentScale,         // New zoom level
          direction: direction,           // Zoom direction ('in' or 'out')
          isWindowResize: currentWidth !== this.lastWidth  // Whether window size changed
        }
      }));

      // Update stored values for next comparison
      this.lastScale = currentScale;
      this.lastWidth = currentWidth;
    }
  }
}

Once you have added this code, all that remains is to initialize it and listen for a zoom event:

// Initialize zoom detector 
const zoomDetector = new ZoomDetector();

// Set up listener for custom zoom events
window.addEventListener('zoom', (e) => {
  // Optionally extract zoom details from event
  const { oldScale, newScale, direction, isWindowResize } = e.detail;
  //
  // Your code to react to zoom activities!
  //
});

The full example from earlier shows all of this code (and some additional code) that shows our zoom detection code in action.

How this Code Works

If we take a 20,000 ft view, the code detects when someone zooms in or out of their browser by using the browser size and viewport size logic we described earlier. When a zoom activity is detected, our code fires a custom zoom event that other code across our app can listen to.

The entirety of this logic lives in the ZoomDetector class where our constructor kicks things off:

constructor() {
  // Store initial window width for comparison
  this.lastWidth = window.innerWidth;
  // Calculate and store initial zoom level
  this.lastScale = this.getZoomLevel();
  // Set up event listeners for zoom detection
  this.setupListeners();
}

The getZoomLevel function gets the current scale value by taking the ratio of the browser width and the viewport width:

getZoomLevel() {
  return Math.round(window.outerWidth / window.innerWidth * 100);
}

For example, if our browser window is 1000 pixels wide and our content area is 800 pixels wide, the zoom level would be: (1000/800) * 100 = 125, meaning we’re zoomed in by 125%. We have no taken care of the initial state of our page.

To detect when zooms happen after the page loads, either via using the Ctrl/Cmd and +/- keys or via a pinch gesture, we have our setupListeners method:

setupListeners() {
  // Listen for browser window resize events (triggered by browser zoom)
  window.addEventListener('resize', () => {
    // Use requestAnimationFrame to optimize performance
    // Ensures zoom check happens during the next frame render
    requestAnimationFrame(() => this.checkZoom());
  });

  // Handle pinch-to-zoom events using visualViewport API if available
  if (window.visualViewport) {
    window.visualViewport.addEventListener('resize', () => {
      // Only process if window width hasn't changed
      // This prevents duplicate events when both viewport and window resize
      if (window.innerWidth === this.lastWidth) {
        this.checkZoom();
      }
    });
  }
}

We listen for the resize event in both zoom cases and call the checkZoom method, where the bulk of our logic for differentiating between a regular old resize and the “what we care about” zoom activity takes place:

checkZoom() {
  // Get current measurements
  const currentScale = this.getZoomLevel();
  const currentWidth = window.innerWidth;

  // Only proceed if there's an actual change in zoom or window size
  if (currentScale !== this.lastScale || currentWidth !== this.lastWidth) {
    // Determine if user is zooming in or out
    const direction = currentScale > this.lastScale ? 'in' : 'out';
    console.log(`Zoom ${direction} detected: ${currentScale}%`);

    // Create and dispatch custom zoom event with detailed information
    window.dispatchEvent(new CustomEvent('zoom', {
      detail: {
        oldScale: this.lastScale,      // Previous zoom level
        newScale: currentScale,         // New zoom level
        direction: direction,           // Zoom direction ('in' or 'out')
        isWindowResize: currentWidth !== this.lastWidth  // Whether window size changed
      }
    }));

    // Update stored values for next comparison
    this.lastScale = currentScale;
    this.lastWidth = currentWidth;
  }
}

Take a few moments to walk through how this code works. When we detect that a zoom activity has indeed taken place, we fire a custom zoom event:

// Create and dispatch custom zoom event with detailed information
window.dispatchEvent(new CustomEvent('zoom', {
  detail: {
    oldScale: this.lastScale,      // Previous zoom level
    newScale: currentScale,         // New zoom level
    direction: direction,           // Zoom direction ('in' or 'out')
    isWindowResize: currentWidth !== this.lastWidth  // Whether window size changed
  }
}));

When this zoom event is fired, all event listeners throughout our app that are listening for this event will get triggered. This is the final task that all of the code we’ve seen build up to!

Conclusion

You may be wondering when you'll ever need to know when a zoom event is taking place. In situations where you want to preserve the exact visual output as much as possible, such as in many 2D or WegGL-based canvas scenarios, it will be very handy to detect zooms and handle its effects. What we saw here is one approach where we can use the browser size and viewport size as a proxy metric to detect when changes happen and determine if those changes were in fact caused by a zoom operation.

Just a final word before we wrap up. If you have a question and/or want to be part of a friendly, collaborative community of over 220k other developers like yourself, post on the forums for a quick response!

Kirupa's signature!

The KIRUPA Newsletter

Thought provoking content that lives at the intersection of design 🎨, development 🤖, and business 💰 - delivered weekly to over a bazillion subscribers!

SUBSCRIBE NOW

Creating engaging and entertaining content for designers and developers since 1998.

Follow:

Popular

Loose Ends

:: Copyright KIRUPA 2025 //--