Responsive JavaScript Without Resize Listeners
Here's a pattern that shows up in almost every aging frontend codebase:
window.addEventListener('resize', () => {
if (window.innerWidth < 768) enableMobileMenu();
else disableMobileMenu();
});
Three problems: it runs dozens of times per second during a drag-resize, the 768 inevitably drifts apart from the CSS breakpoint, and it doesn't run on load without an extra manual call. matchMedia fixes all three.
const mobile = window.matchMedia('(max-width: 767px)');
function apply(mq) {
mq.matches ? enableMobileMenu() : disableMobileMenu();
}
apply(mobile); // initial state
mobile.addEventListener('change', apply); // fires ONLY when crossing 767px
The change event fires exactly once per boundary crossing - not on every pixel of resize. The browser does the watching; your code only runs when the answer changes.
It speaks full media query, not just width
Anything CSS can query, JS can now react to:
// Dark mode toggled at the OS level, live:
matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', e => setTheme(e.matches ? 'dark' : 'light'));
// Respect reduced motion in JS-driven animations:
const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
scroller.scrollTo({ top: 0, behavior: reduceMotion ? 'auto' : 'smooth' });
// Pointer type - show hover hints only where hover exists:
if (matchMedia('(hover: hover)').matches) enableTooltips();
That reduced-motion line belongs in every scroll-behavior and animation call you write - the CSS side of that story is its own topic.
Keeping JS and CSS breakpoints in one place
The drift problem disappears if CSS owns the value and JS reads it:
:root { --bp-mobile: 767px; }
const bp = getComputedStyle(document.documentElement)
.getPropertyValue('--bp-mobile').trim();
const mobile = matchMedia(`(max-width: ${bp})`);
One source of truth, and redesigns stop breaking your JS silently.
When you still need resize
matchMedia answers "which side of a breakpoint am I on". For "how big is this element exactly" (canvas sizing, virtualized lists), the right tool is ResizeObserver on the element itself - window resize listeners are almost never the right answer anymore.
Support: matchMedia itself is ancient (IE10+); the modern addEventListener syntax on it works in everything current, with .addListener as the legacy spelling for very old Safari.
Full-stack web developer sharing practical tutorials and building tools that ship.
Got something on your mind?
My inbox is open - no forms disappearing into the void here.
- Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
- Hire me for a project Have something custom in mind? Let's talk scope and timelines.
- Product support Bought something here? I'll help you get it running.
I usually reply within 1-2 business days.