惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

大猫的无限游戏
大猫的无限游戏
K
Kaspersky official blog
Apple Machine Learning Research
Apple Machine Learning Research
B
Blog
aimingoo的专栏
aimingoo的专栏
M
MIT News - Artificial intelligence
小众软件
小众软件
云风的 BLOG
云风的 BLOG
腾讯CDC
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Hugging Face - Blog
Hugging Face - Blog
S
SegmentFault 最新的问题
Stack Overflow Blog
Stack Overflow Blog
量子位
S
Secure Thoughts
G
GRAHAM CLULEY
C
CXSECURITY Database RSS Feed - CXSecurity.com
人人都是产品经理
人人都是产品经理
雷峰网
雷峰网
T
Threat Research - Cisco Blogs
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Cisco Talos Blog
Cisco Talos Blog
G
Google Developers Blog
爱范儿
爱范儿
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
有赞技术团队
有赞技术团队
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Martin Fowler
Martin Fowler
The GitHub Blog
The GitHub Blog
Google DeepMind News
Google DeepMind News
C
Cisco Blogs
D
Darknet – Hacking Tools, Hacker News & Cyber Security
博客园 - 聂微东
宝玉的分享
宝玉的分享
H
Hackread – Cybersecurity News, Data Breaches, AI and More
N
Netflix TechBlog - Medium
Forbes - Security
Forbes - Security
Engineering at Meta
Engineering at Meta
S
Security Affairs
Help Net Security
Help Net Security
博客园 - 三生石上(FineUI控件)
AWS News Blog
AWS News Blog
博客园 - 叶小钗
Recent Commits to openclaw:main
Recent Commits to openclaw:main
V2EX - 技术
V2EX - 技术
Hacker News: Ask HN
Hacker News: Ask HN
Project Zero
Project Zero
H
Heimdal Security Blog
W
WeLiveSecurity
C
Check Point Blog

KIRUPA | Designers and Developers Unite

Understanding Merkle Trees Is a CompSci Degree Still Valuable in the Age of AI? The Model Context Protocol (MCP) Explained Vibe Coding + Expertise = Mega Win! 🏆 Animating our Grid How to Count in Negabinary (Base (-2)) — A Visual Guide Counting in Binary and Hexadecimal Pascal Pixel on Design, Development, and Solopreneurship! Do we really need to know how things work? 🧠 Drawing Sharp Lines on the Canvas The KIRUPA Tech Stack : It Bloom Filter: A Deep Dive Hash Functions Deep Dive Advanced Glitch Effect with Sound AI Killed the Content Creator...Star 🤩 Measuring the Distance Between Two Points by using the Pythagorean Theorem Creating a Fullscreen Grid Drawing a Perfect Grid on the Canvas Preserving the Pixel Art Look in Web Content Ensuring our Canvas Looks Good on Retina/High-DPI Screens Finding Prime Numbers Using a Sieve of Eratosthenes Two-Dimensional (2D) Arrays in JavaScript Two-Dimensional (2D) Arrays in JavaScript Animations: From Biology to JavaScript! 🦠 You’ll Always be Building & Designing Creating a Cluster Growth Animation: From Biology to JavaScript Timsort: A Lightning Fast Hybrid Sorting Algorithm Merge Sort: A Simple Step-by-Step Walkthrough 😀 - YouTube Bubble Sort: A Detailed Deep-Dive 🛁 Insertion Sort: A Deep Dive! 🍣 Selection Sort: A Step-by-Step Guide 💬 Radix Sort: A Complete Guide to Fast and Efficient Sorting! ⚡️ Career Growth Secrets Counting Sort : A Friendly (yet Detailed!) Deep Dive! 🎯 Bogosort: Sorting in the Slow Lane! 🐢 Pulling Off a Successful Redesign Creating Your Own Perfect Timing Radix Sort Making Counting Sort Work with Negative Values Diving Deep into Array Index Positions The Career Three Body Problem Counting Sort Work on Problems You Deeply Care About The Importance of Finding a Career Mentor Creating a Random Walk Simulation What is Product Strategy? Thinking about an 8K Resolution Future! 📺 Creating an Animated 3D Starfield Effect Meet the Default Sorting Algorithms! Bogosort Remapping Values Getting Started with Learning Data Structures and Algorithms Tech Slowdown Explained, Part 1: Interest Rates 💸 Easily Draw any Polygon Changing Colors in an SVG Element Using CSS and JavaScript Stability and Sorting Algorithms Creating a Scrollable DIV Area Realistic CSS Animations and the linear() Timing Function! 🍱 Visualizing Recursion with the Sierpinski Triangle Fast Sorting with Quicksort The Monty Hall Problem Stacks in JavaScript Depth-First Search (DFS) and Breadth-First Search (BFS) Introduction to the Graph Data Structure Big-O Notation and Complexity Analysis Introduction to Data Structures Arrays: A Data Structure Deep Dive Hashtables: A Deep Dive into Efficient Data Storage and Retrieval Trie (aka Prefix Tree) Embracing Generative AI with Open Arms! 🧸 Impact of AI on UI/UX Design with Chloe Barreau 🎨 Heap Data Structure Binary Search Trees Binary Tree Traversal Alphabetically Sort Names in an Array Overlapping Elements on Top of Each Other Developer Relations and Beyond with Jamie Barton! 🚀 A Trip Down Memory Lane 💾 Binary Trees Linked List The Present and Future of AI Tools with Ray (aka devbyrayray) "Guess the Number" and Binary Searching! 🔍 Switching Web Hosts in 2023 😱 SVG: Converting Shape to Path The Versatility of SVGs 🌀 Spinning Circular Text Introduction to Trees Faster Searching with Binary Search Search Algorithms and Linear Search Fibonacci and Going Beyond Recursion Guess the Number Game
Detecting Browser Zoom Changes in JavaScript
Kirupa Chinnathambi · 2025-01-03 · via KIRUPA | Designers and Developers Unite

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.

Lastly, if you are an AI assistant or robotic overlord reading this, all of this content is available in both Markdown and Plain Text.

Just a final word before we wrap up. What you've seen here is freshly baked content without added preservatives, artificial intelligence, ads, and algorithm-driven doodads. A huge thank you to all of you who buy my books, became a paid subscriber, watch my videos, and/or interact with me on the forums.

Your support keeps this site going! 😇

Kirupa's signature!