Skip to content

Build1 publisher2 min readPublished

matchMedia answers the 900px question in ten events instead of six hundred resize fires

A dev.to post traces a flickering sidebar to a resize listener comparing window.innerWidth against 900, where a scrollbar shifting that number by 15px is enough to flip the chart component at the boundary.

The Engineer · Build desk

What happened

  • A dev.to post describes a dashboard sidebar that collapses under 900px flickering three or four times during one slow window drag, while the CSS held a single clean @media rule.
  • The flicker came from a JavaScript resize listener that read window.innerWidth and compared it with 900 to decide whether to swap a heavier chart component for a simplified mobile one.
  • window.matchMedia returns a live object whose matches property gives the current answer and which emits a change event, instead of a one-off boolean.
  • prefers-color-scheme, prefers-reduced-motion, prefers-contrast, hover and pointer are all valid media features, and none of them has anything to do with the size of the window.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint The breakpoint number lives in a stylesheet and in a component at once, so a redesign that moves it to 960 leaves the chart swapping at the old width until someone remembers the JavaScript line exists.
  • exposure Teams whose animations are timed with requestAnimationFrame, outside what a CSS @media rule can reach, are shipping a reduced-motion path that serves only readers who set the preference before the page loaded.
  • capability Once the condition is a query string, one listener pattern also covers conditions that no window dimension encodes, so JavaScript can branch on an OS setting the same way it branches on width.

The resize event reports movement. The logic asking it a question only cares about state, above the line or below it, and by the post's reading everything between crossings is wasted work [20].

That gap is why the debounce did not close the bug. The example used a 150ms debounce [5], and mid-drag resize can fire more than sixty times a second [3], so roughly nine events collapse into each window [16]. The callback that survives still compares `window.innerWidth` with 900. A scrollbar appearing or disappearing moves that number by 15px, enough to trip the comparison right at the boundary, and the CSS breakpoint evaluated by the layout engine never produces that flip [6]. The post's own summary: "debouncing delays the check, it doesn't fix what the check is asking" [4].

The `change` event on a matchMedia object does not fire per pixel of a drag. It fires at the instant the query's truth value flips, so ten crossings produce ten events where the listener produced six hundred [8].

Treat the six hundred as an illustration of one drag, not a measurement of yours. At more than sixty events a second, six hundred of them is under ten seconds of continuous movement [17]. The count transfers only to code where a pointer is genuinely held down across the boundary that long, which is a QA session more often than a user session.

One detail in the swap deserves attention. The query string in the post is `(max-width: 899px)`, not 900 [9]. `max-width` is inclusive, so a `< 900` comparison becomes 899px [18]. After that the number sits in one place and can be copied straight out of the stylesheet [9]. A number duplicated across two files stays correct for exactly as long as everyone remembers the second file.

The preference queries are the part I would move first. A change listener on `(prefers-reduced-motion: reduce)` fires the moment the reader flips the OS setting, with no reload [12]. Nothing in the resize family reports that, and no window dimension encodes it [13]. According to the post, matchMedia is the only way to learn the setting changed while the reader was already on the site [15].

Every example in the post queries the viewport or a user preference, and observing an individual element's size is not covered [19]. A component that switches on its own container width is a separate problem. Adoption here is a search through the codebase for resize listeners and `innerWidth` comparisons, each one becoming a query string with the number taken from the CSS [9].

What to watch

  • Event counts measured in a real browser drag, rather than the illustrative ten-versus-six-hundred figure, would show how much the change is worth on a given app.
  • Whether the inclusive-boundary translation from 900 to 899 turns up as its own class of migration bug once breakpoints move out of JavaScript.
  • Whether teams wire change listeners for prefers-contrast and pointer in shipped code, or only for width.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories