21

window.devicePixelRatio will return 1 or 2 depending on if I'm using my retina monitor or standard. If I drag the window between the two monitors, this property will change. Is there a way I can have a listener fire when the change occurs?

Yves M.
  • 29,855
  • 23
  • 108
  • 144
Matt Coady
  • 3,418
  • 5
  • 38
  • 63
  • I don't have two monitors to test this but I think the `resize` event will be fired when `window.devicePixelRatio` updates. –  Mar 06 '15 at 20:10
  • 1
    Just tried it, doesn't fire. – Matt Coady Mar 06 '15 at 22:44
  • 1
    There is a good example at developer.mozilla.org [Example 2: Monitoring screen resolution or zoom level changes](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio). – dastrobu Dec 23 '19 at 19:48
  • @user2570380 You don't need two monitors to test this. Just open e.g. Chrome DevTools → Toggle device toolbar (phone/tablet icon at the top left of DevTools) → Triple dot menu → Add device pixel ratio, then change the DPR at will. Note that I use Chrome 81 as of writing. – Michael Johansen May 06 '20 at 22:46
  • @MichaelJohansen: It doesn't trigger for me at all (Windows), it also seems to be buggy currently, see this issue on the Chromium tracker: https://bugs.chromium.org/p/chromium/issues/detail?id=1294293 – strarsis Feb 04 '22 at 19:13

7 Answers7

15

You can listen to a media query with matchMedia that will tell you when the devicePixelRatio goes past a certain barrier (unfortunately not for arbitrary scale changes).

e.g:

window.matchMedia('screen and (min-resolution: 2dppx)')
    .addEventListener("change", function(e) {
      if (e.matches) {
        /* devicePixelRatio >= 2 */
      } else {
        /* devicePixelRatio < 2 */
      }
    });

The listener will be called when you drag a window between monitors, and when plugging in or unplugging an external non-retina monitor (if it causes the window to move from a retina to non-retina screen or vice-versa).

window.matchMedia is supported in IE10+, and all other modern browsers.

References: https://code.google.com/p/chromium/issues/detail?id=123694, MDN on min-resolution

nornagon
  • 15,393
  • 18
  • 71
  • 85
10

Most (or all?) answers on the internet only detect a specific change. Typically they detect whether the value is 2 or something else.

The issue probably lies in the MediaQuery, because they only allow checking for specific hardcoded values.

With some programming, it's possible to dynamically create a media query, which checks for a change of the current value.

let remove = null;

const updatePixelRatio = () => {
  if(remove != null) {
      remove();
  }
  let mqString = `(resolution: ${window.devicePixelRatio}dppx)`;
  let media = matchMedia(mqString);
  media.addListener(updatePixelRatio);
  remove = function() {media.removeListener(updatePixelRatio)};

  console.log("devicePixelRatio: " + window.devicePixelRatio);
}
updatePixelRatio();
  • Smart, but isn't this repeatedly creating new `MediaQueryList`s that just remain out here? – Jean-Philippe Pellet Sep 14 '21 at 08:35
  • That's a good point. It probably doesn't causes a leak, because I remove the listener, and this seems to be the only way to dispose it. But it's dependent on how the browser have implemented this feature. – Florian Kirmaier Sep 16 '21 at 12:45
6

I took the IMO best answer (by @Neil) and made it a bit more human-readable:

function listenOnDevicePixelRatio() {
  function onChange() {
    console.log("devicePixelRatio changed: " + window.devicePixelRatio);
    listenOnDevicePixelRatio();
  }
  matchMedia(
    `(resolution: ${window.devicePixelRatio}dppx)`
  ).addEventListener("change", onChange, { once: true });
}
listenOnDevicePixelRatio();

No fixed boundary or variables needed.

EzPizza
  • 979
  • 1
  • 13
  • 22
  • 3
    This approach also matches the documentation in MDN: https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#monitoring_screen_resolution_or_zoom_level_changes – jtbandes Jun 28 '22 at 18:40
4

Thanks @florian-kirmaier this is exactly what I was looking for and if you pass in the option {once: true} in the event listener there is no need to manually keep track and remove the event listener.

(function updatePixelRatio(){
matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
.addEventListener('change', updatePixelRatio, {once: true});
console.log("devicePixelRatio: " + window.devicePixelRatio);
})();
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
Neil
  • 41
  • 3
0

I prefer this one, so that I can provide a callback, and for the callback not to fire initially but only on changes, and to be able to stop it when no longer needed:

function onPixelRatioChange(cb) {
    let mediaQuery
    const listenerOptions = { once: true }
    let firstRun = true

    function onChange() {
        if (firstRun) firstRun = false
        else cb()

        mediaQuery = matchMedia(`(resolution: ${devicePixelRatio}dppx)`)
        mediaQuery.addEventListener('change', onChange, listenerOptions)
    }

    onChange()

    return function unlisten() {
        mediaQuery.removeEventListener('change', onChange, listenerOptions)
    }
}

// Then use it like this:

const unlisten = onPixelRatioChange(() => {
    console.log('pixel ratio changed:', devicePixelRatio)
})

// later, stop listening if desired:
unlisten()
trusktr
  • 44,284
  • 53
  • 191
  • 263
0

Here's a typescript object version of @Florian's answer

export default class DevicePixelRatioObserver {
    mediaQueryList: MediaQueryList | null = null

    constructor(readonly onDevicePixelRatioChanged: () => void) {
        this._onChange = this._onChange.bind(this)
        this.createMediaQueryList()
    }

    createMediaQueryList() {
        this.removeMediaQueryList()
        let mqString = `(resolution: ${window.devicePixelRatio}dppx)`;

        this.mediaQueryList = matchMedia(mqString);
        this.mediaQueryList.addEventListener('change', this._onChange)
    }
    removeMediaQueryList() {
        this.mediaQueryList?.removeEventListener('change', this._onChange)
        this.mediaQueryList = null
    }
    _onChange(event: MediaQueryListEvent) {
        this.onDevicePixelRatioChanged()
        this.createMediaQueryList()
    }
    destroy() {
        this.removeMediaQueryList()
    }
}
joerick
  • 16,078
  • 4
  • 53
  • 57
0

I made a function that will watch pixel ratio changes and will return a 'stop watching' function:

It also allows passing custom 'targetWindow' if you work with multi window setup.

function watchDevicePixelRatio(callback: (ratio: number) => void, targetWindow: Window = window) {
  const media = targetWindow.matchMedia(`(resolution: ${targetWindow.devicePixelRatio}dppx)`);

  function handleChange() {
    callback(targetWindow.devicePixelRatio);
  }

  media.addEventListener("change", handleChange);

  return function stopWatching() {
    media.removeEventListener("change", handleChange);
  };
}
Adam Pietrasiak
  • 12,773
  • 9
  • 78
  • 91