Table of Contents
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!
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!
When we look at a typical web page, there are two sizes at play:
The two sizes are:
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.
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.
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.
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!
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!
:: Copyright KIRUPA 2025 //--