How JavaScript Can Detect Page Zoom

Interactive examples for comparing devicePixelRatio, matchMedia, outerWidth / innerWidth, and visualViewport.scale.

1. resize Event + devicePixelRatio Change

Code:
let lastPixelRatio = window.devicePixelRatio;

function checkResizePixelRatio() {
  let currentPixelRatio = window.devicePixelRatio;

  if (currentPixelRatio !== lastPixelRatio) {
    resizeDprLog.textContent =
      'The page zoom has changed. DPR: ' + currentPixelRatio;
  }

  lastPixelRatio = currentPixelRatio;
}

window.addEventListener('resize', checkResizePixelRatio);
resizeDprButton.addEventListener('click', checkResizePixelRatio);
Result:
Current DPR-
Previous DPR-
Resize events0
StatusWaiting
Zoom the page with Ctrl+/Ctrl- or use the buttons to inspect the current value.

2. Estimating Zoom from a Stored Baseline

Code:
// Initial zoom ratio
let originPixelRatio = localStorage.devicePixelRatio;

if (!originPixelRatio) {
  originPixelRatio = window.devicePixelRatio;

  if (Number.isInteger(originPixelRatio)) {
    localStorage.devicePixelRatio = originPixelRatio;
  }
}

let storedLastPixelRatio = originPixelRatio;

function estimateStoredZoom() {
  let currentPixelRatio = window.devicePixelRatio;
  let zoom = Math.round(1000 * (currentPixelRatio / originPixelRatio)) / 10;

  storedZoomResult.textContent = 'The zoom ratio is: ' + zoom + '%';
  storedLastPixelRatio = currentPixelRatio;
}

window.addEventListener('resize', estimateStoredZoom);
storedZoomButton.addEventListener('click', estimateStoredZoom);
Result:
Stored baseline-
Current DPR-
Estimated zoom-
Storage-
Waiting for a zoom or manual check.

3. matchMedia with a dppx Resolution Query

Code:
let mediaOriginPixelRatio = window.devicePixelRatio;
let mediaQueryList;

function mediaPixelRatioChanged() {
  let currentPixelRatio = window.devicePixelRatio;
  let zoom = Math.round(1000 * (currentPixelRatio / mediaOriginPixelRatio)) / 10;

  matchMediaResult.textContent = 'The zoom ratio is: ' + zoom + '%';

  mediaQueryList.removeEventListener('change', mediaPixelRatioChanged);
  mediaQueryList = matchMedia('(resolution: ' + currentPixelRatio + 'dppx)');
  mediaQueryList.addEventListener('change', mediaPixelRatioChanged);
}

mediaQueryList = matchMedia('(resolution: ' + mediaOriginPixelRatio + 'dppx)');
mediaQueryList.addEventListener('change', mediaPixelRatioChanged);
matchMediaButton.addEventListener('click', mediaPixelRatioChanged);
Result:
Watched query-
Matches now-
Current DPR-
Change events0
A resolution media query is watching for DPR changes.

4. outerWidth / innerWidth Ratio

Code:
function calculateOuterInnerZoom() {
  let zoom = Math.round(1000 * (window.outerWidth / window.innerWidth)) / 10;

  outerInnerResult.textContent =
    'The current page zoom ratio should be: ' + zoom + '%';
}

window.addEventListener('resize', calculateOuterInnerZoom);
outerInnerButton.addEventListener('click', calculateOuterInnerZoom);
Result:
outerWidth-
innerWidth-
Ratio-
RiskSidebar sensitive
This estimate changes when browser chrome, sidebars, or docked developer tools affect innerWidth.

5. visualViewport and Gesture Zoom

Code:
function showGestureZoom() {
  if (!window.visualViewport) {
    visualViewportResult.textContent = 'visualViewport is not supported.';
    return;
  }

  visualViewportResult.textContent =
    'Gesture zoom ratio: ' + window.visualViewport.scale;
}

if (window.visualViewport) {
  window.visualViewport.addEventListener('resize', showGestureZoom);
}

visualViewportButton.addEventListener('click', showGestureZoom);
Result:
visualViewport.scale -

Checking support...

On desktop browser zoom this usually stays at 1. On mobile pinch zoom it can change.

6. Final Summary

Code:
const methods = [
  { name: 'resize + devicePixelRatio', bestFor: 'Desktop zoom change detection' },
  { name: 'matchMedia resolution query', bestFor: 'Targeted desktop DPR change detection' },
  { name: 'outerWidth / innerWidth', bestFor: 'Rough desktop cross-checks' },
  { name: 'visualViewport.scale', bestFor: 'Mobile gesture zoom' }
];

summaryResult.innerHTML = methods.map(method =>
  '<li><strong>' + method.name + '</strong>' + method.bestFor + '</li>'
).join('');
Result: