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
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.
.matchestells you the current result.- The
changeevent 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
| Query | Tells 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
| Tool | Watches | Notes |
|---|---|---|
window resize event | The browser window | Fires many times a second while dragging; debounce it |
matchMedia change | A breakpoint | Fires once when crossing it; best for layout switches |
ResizeObserver | Any element's size | Catches changes from sidebars opening, fonts loading or content growing |
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
| Value | Means |
|---|---|
window.innerWidth | Viewport width including the scrollbar |
document.documentElement.clientWidth | Viewport width without the scrollbar |
screen.width | The whole physical screen, rarely what you want |
window.devicePixelRatio | Device pixels per CSS pixel |
visualViewport.height | Visible 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).matcheschecks a media query; itschangeevent fires when the result flips.- Use the same breakpoints in JavaScript as in CSS.
- Detect touch with
(pointer: coarse)and preferences withprefers-reduced-motionandprefers-color-scheme. - The window
resizeevent fires constantly; debounce it or usematchMediainstead. ResizeObserverreacts 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.
Switching behaviour at a breakpoint
Drag the result panel narrower and wider than 600px.
<p id="mode"></p>
<nav id="nav">
<button id="menu-btn">Menu</button>
<ul id="links"><li>Home</li><li>Shop</li><li>Help</li></ul>
</nav>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; });Line-by-line explanation
- Line 1The same breakpoint the CSS would use.
- Lines 5 to 9One function sets up the page for either layout.
- Line 11Apply the right layout immediately on load.
- Lines 12 to 15Re-apply only when the breakpoint is crossed, not on every pixel.
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");Line-by-line explanation
- Lines 2 to 6Each preference is one media query.
- Line 11Turn animation off for people who prefer reduced motion.
- Lines 13 to 14Choose interactions based on how the person is actually using the device.
ResizeObserver on a single element
Drag the box's bottom-right corner to resize it.
<div id="box">Resize me</div>
<style>
#box { resize: horizontal; overflow: auto; width: 260px; min-width: 120px; max-width: 100%; padding: 16px; border: 2px dashed #94a3b8; font-family: sans-serif; }
#box.compact { background: #fef3c7; }
#box.roomy { background: #dcfce7; }
</style>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);Line-by-line explanation
- Line 3The callback runs whenever the observed element changes size, for any reason.
- Line 5
contentRect.widthis the content width in pixels. - Lines 6 to 8Switch styles and text based on the box's own width.
- Line 12Start observing; call
observer.disconnect()to stop.
Common Responsive JavaScript Mistakes
Detecting phones from the user agent
const isMobile = /iPhone|Android/.test(navigator.userAgent); // wrong for tablets, foldables, touch laptopsconst isTouch = matchMedia("(pointer: coarse)").matches;
const isSmall = matchMedia("(max-width: 600px)").matches;Heavy work on every resize event
window.addEventListener("resize", () => rebuildLayout()); // runs dozens of times while draggingmatchMedia("(min-width: 768px)").addEventListener("change", (e) => rebuildLayout(e.matches));Only checking once
if (matchMedia("(min-width: 768px)").matches) setupDesktop(); // breaks when rotated or resizedconst 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.
Check whether the screen is at least 1024px wide.
const isLarge = window.("(min-width: 1024px)").matches;
console.log(isLarge);
matchMedia runs a CSS media query from JavaScript.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"));Which query detects that the user prefers less animation?
Which tool reacts when a sidebar opens and a chart's container gets narrower, without the window changing size?
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));- Uses a valid media query
- Calls back immediately
- Listens for change on the query
Responsive JavaScript Quiz
5 questions to check what you learned.
What does matchMedia("(max-width: 600px)").matches return?
- The screen width
- A CSS rule
- An event
- true or false
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
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
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
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:
slidesFor()returns3if(min-width: 900px)matches,2if(min-width: 600px)matches, otherwise1- Write
update()that sets#infotext to`Showing ${slidesFor()} slides` - Call
update()once on load - Call
update()again whenever either media query changes, using theirchangeevents
<p id="info"></p>- Picks the right count for this width
- Shows the count on load
- Listens to both queries