Responsive JavaScript: matchMedia, Resize and ResizeObserver

Make scripts adapt to screen size, orientation, touch input and user preferences with matchMedia, the resize event, ResizeObserver and pointer media queries.

Lesson 44 of 56Intermediate16 min read

Responsive design is mostly a CSS job: media queries, flexible grids and fluid images rearrange the page for each screen. But sometimes JavaScript needs to know too. A carousel might show one slide on phones and three on desktops. A navigation script should only run its mobile menu code on small screens. A chart needs to redraw when its container changes size. An animation should calm down for people who've asked their device to reduce motion.

The key tool is window.matchMedia, which runs any CSS media query from JavaScript and tells you whether it matches, and when that changes. For element sizes there's ResizeObserver, which notices when a specific element grows or shrinks, whatever the reason.

In this lesson you'll use both, learn why the plain resize event is usually the wrong tool, detect touch-friendly devices the right way (not by guessing from screen width or browser name), and respond to user preferences like dark mode and reduced motion. The aim is scripts that respond to the same breakpoints as your CSS, so the two never disagree.

matchMedia: media queries in JavaScript

JavaScript
const desktop = window.matchMedia("(min-width: 768px)");

console.log(desktop.matches);   // true or false right now

desktop.addEventListener("change", (e) => {
  console.log(e.matches ? "Desktop layout" : "Mobile layout");
});
  • Pass any CSS media query string, exactly as you'd write it in CSS.
  • .matches tells you the current result.
  • The change event fires only when the result flips, for example as the window crosses 768px, not on every pixel of resizing.
  • Use the same breakpoint values as your CSS, ideally from one shared list.

Useful queries beyond width

QueryTells you
(prefers-color-scheme: dark)The user's device is in dark mode
(prefers-reduced-motion: reduce)The user wants less animation
(orientation: portrait)The screen is taller than it is wide
(hover: hover)The main input can hover, like a mouse
(pointer: coarse)The main input is imprecise, like a finger
(any-pointer: coarse)Any input is a finger, such as a laptop with a touch screen
(display-mode: standalone)The site is running as an installed web app

Checking (pointer: coarse) is far more reliable than guessing "mobile" from screen width or the browser's user-agent string. Tablets, foldables and touch laptops make those guesses wrong.

resize vs ResizeObserver

ToolWatchesNotes
window resize eventThe browser windowFires many times a second while dragging; debounce it
matchMedia changeA breakpointFires once when crossing it; best for layout switches
ResizeObserverAny element's sizeCatches changes from sidebars opening, fonts loading or content growing
JavaScript
const ro = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const width = entry.contentRect.width;
    entry.target.classList.toggle("wide", width > 500);
  }
});
ro.observe(document.querySelector(".chart"));

ResizeObserver is the JavaScript partner of CSS container queries: it responds to the space a component actually has.

Screen and viewport sizes

ValueMeans
window.innerWidthViewport width including the scrollbar
document.documentElement.clientWidthViewport width without the scrollbar
screen.widthThe whole physical screen, rarely what you want
window.devicePixelRatioDevice pixels per CSS pixel
visualViewport.heightVisible height, accounting for on-screen keyboards and pinch zoom

Prefer matchMedia for decisions, and read sizes only when you need exact numbers, such as sizing a canvas.

Responsive JavaScript Syntax

Responsive JavaScript

const mq = matchMedia("(min-width: 768px)");
mq.matches;
mq.addEventListener("change", (e) => setup(e.matches));

const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
const touch = matchMedia("(pointer: coarse)").matches;

new ResizeObserver(([entry]) => draw(entry.contentRect.width)).observe(box);

Responsive JavaScript Summary

  • matchMedia(query).matches checks a media query; its change event fires when the result flips.
  • Use the same breakpoints in JavaScript as in CSS.
  • Detect touch with (pointer: coarse) and preferences with prefers-reduced-motion and prefers-color-scheme.
  • The window resize event fires constantly; debounce it or use matchMedia instead.
  • ResizeObserver reacts to an individual element's size, wherever the change comes from.

Responsive JavaScript Examples

Each example runs in the page. Edit the code and press Run to see what changes.

Example 1

Switching behaviour at a breakpoint

Drag the result panel narrower and wider than 600px.

const wide = window.matchMedia("(min-width: 600px)");
const btn = document.querySelector("#menu-btn");
const links = document.querySelector("#links");

function apply(isWide) {
  document.querySelector("#mode").textContent = isWide ? "Wide: links always shown" : "Narrow: links behind the Menu button";
  btn.hidden = isWide;
  links.hidden = !isWide;
}

apply(wide.matches);
wide.addEventListener("change", (e) => {
  apply(e.matches);
  console.log("Breakpoint crossed:", e.matches ? "wide" : "narrow");
});
btn.addEventListener("click", () => { links.hidden = !links.hidden; });
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Line 1The same breakpoint the CSS would use.
  2. Lines 5 to 9One function sets up the page for either layout.
  3. Line 11Apply the right layout immediately on load.
  4. Lines 12 to 15Re-apply only when the breakpoint is crossed, not on every pixel.
Example 2

Respecting user preferences and input type

const prefs = {
  dark: matchMedia("(prefers-color-scheme: dark)").matches,
  reducedMotion: matchMedia("(prefers-reduced-motion: reduce)").matches,
  touch: matchMedia("(pointer: coarse)").matches,
  canHover: matchMedia("(hover: hover)").matches,
  portrait: matchMedia("(orientation: portrait)").matches,
};

console.log(prefs);

const animationTime = prefs.reducedMotion ? 0 : 400;
console.log(`Animations will last ${animationTime}ms`);
console.log(prefs.canHover ? "Show tooltips on hover" : "Show tooltips on tap");
console.log(prefs.touch ? "Use larger tap targets" : "Standard targets are fine");
ConsoleEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Lines 2 to 6Each preference is one media query.
  2. Line 11Turn animation off for people who prefer reduced motion.
  3. Lines 13 to 14Choose interactions based on how the person is actually using the device.
Example 3

ResizeObserver on a single element

Drag the box's bottom-right corner to resize it.

const box = document.querySelector("#box");

const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const w = Math.round(entry.contentRect.width);
    entry.target.classList.toggle("compact", w < 220);
    entry.target.classList.toggle("roomy", w >= 220);
    entry.target.textContent = `${w}px wide: ${w < 220 ? "compact" : "roomy"} layout`;
  }
});

observer.observe(box);
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Line 3The callback runs whenever the observed element changes size, for any reason.
  2. Line 5contentRect.width is the content width in pixels.
  3. Lines 6 to 8Switch styles and text based on the box's own width.
  4. Line 12Start observing; call observer.disconnect() to stop.

Common Responsive JavaScript Mistakes

Detecting phones from the user agent

Don't
const isMobile = /iPhone|Android/.test(navigator.userAgent); // wrong for tablets, foldables, touch laptops
Do
const isTouch = matchMedia("(pointer: coarse)").matches;
const isSmall = matchMedia("(max-width: 600px)").matches;

Heavy work on every resize event

Don't
window.addEventListener("resize", () => rebuildLayout()); // runs dozens of times while dragging
Do
matchMedia("(min-width: 768px)").addEventListener("change", (e) => rebuildLayout(e.matches));

Only checking once

Don't
if (matchMedia("(min-width: 768px)").matches) setupDesktop(); // breaks when rotated or resized
Do
const mq = matchMedia("(min-width: 768px)");
const apply = () => (mq.matches ? setupDesktop() : setupMobile());
apply();
mq.addEventListener("change", apply);

Responsive JavaScript in Real Projects

Responsive scripts in practice

Carousel libraries change the number of visible slides with matchMedia breakpoints. Map and chart libraries use ResizeObserver to redraw when their container changes. Video players switch to touch-friendly controls with (pointer: coarse), and well-built sites skip parallax and autoplaying animations when prefers-reduced-motion is set.

Responsive JavaScript Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Check whether the screen is at least 1024px wide.

const isLarge = window.("(min-width: 1024px)").matches;
console.log(isLarge);
Predict the outputmedium

The window starts at 500px wide and is dragged slowly to 900px. How many times does this log?

matchMedia("(min-width: 768px)").addEventListener("change", () => console.log("changed"));
Multiple choiceeasy

Which query detects that the user prefers less animation?

Multiple choicemedium

Which tool reacts when a sidebar opens and a chart's container gets narrower, without the window changing size?

Fix the bugmedium

watchLayout(callback) should call callback(true) or callback(false) immediately for whether the viewport is at least 700px wide, then again each time that changes. It should return the MediaQueryList. Fix it.

function watchLayout(callback) {
  const mq = window.matchMedia("min-width: 700px");
  window.addEventListener("resize", () => callback(mq.matches));
  return mq;
}
watchLayout((wide) => console.log("wide?", wide));
ConsoleEdit the code, then press Run or Ctrl+Enter
Checks
  • Uses a valid media query
  • Calls back immediately
  • Listens for change on the query

Responsive JavaScript Quiz

5 questions to check what you learned.

  1. What does matchMedia("(max-width: 600px)").matches return?

    • The screen width
    • A CSS rule
    • An event
    • true or false
  2. How do you best detect a touch-first device?

    • Check if the width is under 768px
    • Look for "Mobile" in the user agent
    • matchMedia("(pointer: coarse)")
    • Check for a keyboard
  3. Why should JavaScript use the same breakpoints as CSS?

    • So the behaviour and the layout always switch together
    • It's faster
    • Browsers require it
    • To use less memory
  4. When does a MediaQueryList's change event fire?

    • Every time the window resizes
    • Once on page load
    • When the query's result changes from true to false or back
    • Only on phones
  5. What does entry.contentRect.width give in a ResizeObserver callback?

    • The window width
    • The width including margins
    • The screen width
    • The element's content box width in pixels

Responsive JavaScript Challenge

A responsive slide count

Write slidesFor() and keep #info up to date:

  1. slidesFor() returns 3 if (min-width: 900px) matches, 2 if (min-width: 600px) matches, otherwise 1
  2. Write update() that sets #info text to `Showing ${slidesFor()} slides`
  3. Call update() once on load
  4. Call update() again whenever either media query changes, using their change events
ResultEdit the code, then press Run or Ctrl+Enter
Checks
  • Picks the right count for this width
  • Shows the count on load
  • Listens to both queries