window.devicePixelRatio change listener
Asked Answered
P

7

24

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?

Padova answered 6/3, 2015 at 18:38 Comment(5)
I don't have two monitors to test this but I think the resize event will be fired when window.devicePixelRatio updates.Cynara
Just tried it, doesn't fire.Padova
There is a good example at developer.mozilla.org Example 2: Monitoring screen resolution or zoom level changes.Religiose
@Cynara 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.Moonwort
@MichaelJohansen: It doesn't trigger for me at all (Windows), it also seems to be buggy currently, see this issue on the Chromium tracker: bugs.chromium.org/p/chromium/issues/detail?id=1294293Cusk
G
12

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.

Gothar answered 6/4, 2022 at 21:52 Comment(1)
This approach also matches the documentation in MDN: developer.mozilla.org/en-US/docs/Web/API/Window/…Witching
C
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

Christiechristin answered 15/4, 2015 at 15:0 Comment(3)
Thanks, I've edited the answer to reflect the new method.Christiechristin
This is much worse than the other answers, because it only allows for fixed boundariesGothar
@James could you delete your comment, because it relates to an old version of the answerTsan
I
12

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();
Irrawaddy answered 26/4, 2021 at 9:20 Comment(4)
Smart, but isn't this repeatedly creating new MediaQueryLists that just remain out here?Ruebenrueda
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.Irrawaddy
developer.mozilla.org/en-US/docs/Web/API/Window/…Iaverne
Now, the MDN page is correct. :) When answering the question, I did create a ticket. github.com/mdn/content/issues/4478 They had fixed it about a year ago.Irrawaddy
G
12

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.

Gothar answered 6/4, 2022 at 21:52 Comment(1)
This approach also matches the documentation in MDN: developer.mozilla.org/en-US/docs/Web/API/Window/…Witching
L
7

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);
})();
Libby answered 29/12, 2021 at 2:52 Comment(0)
A
1

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);
  };
}
Arrester answered 7/3, 2023 at 8:54 Comment(0)
H
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()
Harbour answered 28/10, 2022 at 20:51 Comment(0)
D
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()
    }
}
Discophile answered 7/12, 2022 at 17:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.