Media Queries

📱 Chapter 7 — Media Queries and Responsive Layouts

Responsive design is the practice of making a single HTML document adapt to different screen sizes, device capabilities, and user preferences — without serving different HTML to different devices. The core tool is the CSS media query: a conditional block of CSS that applies only when specific conditions about the environment are true. This chapter covers the full media query syntax, the mobile-first philosophy, breakpoint strategy, preference-based queries, and responsive layout patterns that work in production.

1 — The Viewport Meta Tag (Non-Negotiable)

Before a single media query works on a mobile device, you need this tag in your HTML <head>:

📄 HTML — viewport meta tag
<meta name="viewport" content="width=device-width, initial-scale=1">
Without this tag, mobile browsers render the page at a "virtual viewport" of ~980px and then zoom out to show the full page — completely bypassing your media queries. This single line is the prerequisite for all responsive work.
Why does the virtual viewport exist? Before responsive design existed, websites were built for 980px desktops. When smartphones appeared, mobile browsers had to show those sites somehow — they invented a virtual desktop-sized viewport and zoomed out. Now that responsive design exists, width=device-width tells the browser to use the real physical screen width instead, making your media queries meaningful.
  • width=device-width — set the viewport to the device's actual width in CSS pixels.
  • initial-scale=1 — do not zoom in or out when the page first loads.
  • Do not add user-scalable=no — it prevents users from zooming, which is an accessibility violation.

2 — @media Syntax

A media query wraps a block of CSS that only applies when its condition evaluates to true. The condition is made up of a media type (optional) and one or more media features in parentheses.

🎨 CSS — anatomy of a media query
/* @media [type] [and (feature: value)] { declarations } */ @media screen and (min-width: 768px) { /* applies on screens 768px or wider */ } /* Media type is optional — defaults to 'all' */ @media (min-width: 768px) { /* same effect — 'all' is the default type */ } /* Print stylesheet — only when printing */ @media print { .no-print { display: none; } body { font-size: 12pt; color: #000; } }

Media types

TypeApplies to
allAll devices (default when type is omitted)
screenScreens: computers, phones, tablets
printPrint preview and printing
speechScreen readers (rarely used directly)

3 — Width-Based Media Features

min-width and max-width are by far the most used media features. They test the viewport width — the width of the browser window, not the screen resolution.

🎨 CSS — width features
/* min-width: applies when viewport is AT LEAST this wide */ @media (min-width: 600px) { /* 600px and wider */ } /* max-width: applies when viewport is AT MOST this wide */ @media (max-width: 599px) { /* 599px and narrower */ } /* Targeting a range (old syntax) */ @media (min-width: 600px) and (max-width: 959px) { /* tablet only */ } /* Targeting a range (Level 4 range syntax — see Section 7) */ @media (600px <= width <= 959px) { /* same — cleaner */ } /* height, orientation */ @media (min-height: 800px) { /* tall viewports */ } @media (orientation: landscape) { /* wider than tall */ } @media (orientation: portrait) { /* taller than wide */ }
Viewport width is measured in CSS pixels, not physical pixels. On a Retina (2×) display, a 750-pixel-wide phone has a CSS viewport width of 375px. Media queries always use CSS pixels.
Layout simulator — click to see different breakpoints
≈ 360px (phone)
nav
main content
sidebar
aside

4 — Mobile-First vs Desktop-First

Mobile-first
Start small — add with min-width
Write the base CSS for the smallest viewport. Use min-width queries to progressively add complexity as the screen gets wider. Each breakpoint adds to what was already there.
Desktop-first
Start large — strip with max-width
Write the base CSS for large screens. Use max-width queries to remove or simplify as the screen gets smaller. Each breakpoint takes away from the default.
Side-by-side — same result, two philosophies
📱 Mobile-first (min-width)
/* Base: mobile — single column, stacked nav */ .nav { display: flex; flex-direction: column; } .layout { display: grid; grid-template-columns: 1fr; } /* Tablet and above — start showing sidebar */ @media (min-width: 600px) { .layout { grid-template-columns: 2fr 1fr; } } /* Desktop and above — horizontal nav, 3 cols */ @media (min-width: 960px) { .nav { flex-direction: row; } .layout { grid-template-columns: 140px 1fr 120px; } }
Mobile-first is the professional default — and for good reason. Mobile users are the majority of web traffic. Writing for mobile first means your base styles are simpler (less CSS = faster download). Desktop enhancements are layered on top. Mobile-first also tends to produce cleaner cascades: you add properties, rather than overriding and removing them. Desktop-first leads to more unset and none overrides.

5 — Breakpoint Strategy

Content-based vs device-based breakpoints

The most common mistake with breakpoints is choosing them by device ("768 for iPad", "375 for iPhone"). Devices change every year — your CSS should not be tied to 2024 screen sizes. Instead, choose breakpoints where your content breaks: drag the browser window narrow until the layout looks bad, then add a breakpoint there.

🎨 CSS — common breakpoint values (content-based)
/* A reasonable starting set — adjust to suit your content */ @media (min-width: 480px) { /* large phones, landscape */ } @media (min-width: 640px) { /* small tablets, phablets */ } @media (min-width: 768px) { /* tablets */ } @media (min-width: 1024px) { /* small laptops */ } @media (min-width: 1280px) { /* desktops */ } @media (min-width: 1536px) { /* wide screens */ } /* You almost certainly don't need all six — start with two or three. */ /* Most layouts need: small (base) + medium (600-768) + large (960+). */
💡 Use custom properties to store breakpoint values. CSS custom properties cannot be used inside @media condition strings, but you can define them as reference tokens in a comment or use a preprocessor variable. In plain CSS, define breakpoints in one place (a :root comment block or a dedicated _breakpoints.css file) and never write the pixel values from memory — search-and-replace beats trying to remember whether you used 768 or 769.

6 — Logical Operators

🎨 CSS — and, comma (or), not
/* and — ALL conditions must be true */ @media (min-width: 600px) and (orientation: landscape) { /* 600px+ AND landscape orientation */ } /* , (comma = OR) — ANY condition is true */ @media (max-width: 480px), (orientation: portrait) { /* narrow screens OR portrait — either triggers this */ } /* not — negates the entire query (not just the feature) */ @media not (hover: hover) { /* devices without a hover capability (touch-only) */ } /* Combining all three */ @media screen and (min-width: 1024px), print and (min-resolution: 300dpi) { /* wide screen OR high-res print */ }
not pitfall: not negates the entire query, not just the next condition. @media not screen and (min-width: 768px) is read as not (screen and (min-width: 768px)) — it applies to everything that is NOT (a screen wider than 768px). Wrap with parentheses in complex queries.

7 — Level 4 Range Syntax

CSS Media Queries Level 4 introduced a cleaner range syntax using comparison operators. It is supported in all modern browsers since 2023 and eliminates the awkward min-width/max-width naming.

🎨 CSS — range syntax comparison
/* ── Old syntax ── */ @media (min-width: 600px) { } @media (max-width: 599px) { } @media (min-width: 600px) and (max-width: 959px) { } /* ── New range syntax ── */ @media (width >= 600px) { } @media (width < 600px) { } @media (600px <= width < 960px) { } /* Read the last one as: "when 600px ≤ width < 960px" */ /* The range syntax also supports height, aspect-ratio: */ @media (45rem <= width <= 80rem) { /* rem-based breakpoints */ }
Many developers prefer rem-based breakpoints over px: if the user has set a larger browser default font size, rem breakpoints scale with it. 48rem ≈ 768px at the default 16px base, but scales proportionally with user font size preferences.

8 — User Preference Queries

Beyond layout, media queries can detect user preferences set at the operating system level. These unlock genuinely responsive experiences — not just different layouts, but fundamentally different visual treatments based on what the user has told their device they need.

prefers-color-scheme
OS dark/light mode preference. The most widely implemented preference query — all major platforms expose this.
dark · light
prefers-reduced-motion
The user has turned on "Reduce Motion" in accessibility settings. Disable or minimise animations.
reduce · no-preference
hover
Whether the primary input can hover over elements. none = touch-only device. Use to show/hide hover-dependent UI.
hover · none
pointer
Precision of the primary input. coarse = finger/stylus (needs bigger tap targets). fine = mouse/trackpad.
fine · coarse · none
prefers-contrast
The user prefers higher (or lower) contrast than normal. Useful for accessibility overlays.
more · less · no-preference
forced-colors
Windows High Contrast mode is active. The OS forces a limited colour palette on all elements.
active · none
🎨 CSS — preference queries in practice
/* ── Dark mode ── */ :root { --bg: #fff; --text: #111; } @media (prefers-color-scheme: dark) { :root { --bg: #0d0f18; --text: #e2e4ec; } } /* All elements that use var(--bg) and var(--text) switch automatically */ /* ── Reduced motion — IMPORTANT for accessibility ── */ .card { transition: transform 0.3s ease; } .card:hover { transform: translateY(-4px); } @media (prefers-reduced-motion: reduce) { /* Replace motion with a simpler fade, or remove it */ .card { transition: opacity 0.15s; } .card:hover { transform: none; opacity: 0.85; } } /* ── Touch targets for coarse pointer ── */ .btn { padding: 6px 14px; } /* default: fine pointer */ @media (pointer: coarse) { .btn { padding: 12px 20px; min-height: 44px; } /* 44px = Apple's tap target minimum */ } /* ── Hover: hide hover-only UI from touch devices ── */ .tooltip { display: none; } @media (hover: hover) { .tooltip-trigger:hover .tooltip { display: block; } }
⚠️ prefers-reduced-motion is an accessibility requirement. Some users with vestibular disorders experience nausea, vertigo, or migraines from animated interfaces. prefers-reduced-motion: reduce is set by real users who need it. Implementing it is a matter of basic accessibility — not just a nice-to-have enhancement.
💡 Testing preference queries in DevTools. In Chrome: DevTools → more options (⋮) → Rendering tab → scroll to "Emulate CSS media feature". You can force prefers-color-scheme: dark, prefers-reduced-motion: reduce, and more without changing your OS settings. Firefox has the same in DevTools → Inspector → Responsive Design Mode.

9 — Responsive Layout Patterns

Pattern 1 — Stack to side-by-side

🎨 CSS — the most common responsive transformation
/* Mobile: stacked */ .page { display: grid; grid-template-columns: 1fr; gap: 20px; } @media (min-width: 720px) { .page { grid-template-columns: 1fr 300px; grid-template-areas: "main sidebar"; } }

Pattern 2 — Responsive navigation

🎨 CSS — nav that collapses on mobile
/* Mobile: stacked links, hidden by default */ .nav__links { display: none; flex-direction: column; } .nav__links.open { display: flex; } .nav__toggle { display: block; } /* hamburger button */ @media (min-width: 768px) { .nav__links { display: flex !important; /* always visible on desktop */ flex-direction: row; } .nav__toggle { display: none; } /* no hamburger on desktop */ }
The !important on the desktop nav is one of the legitimate uses from Chapter 6: it prevents JS toggle logic (which sets display: flex via class) from accidentally hiding the nav when resizing from mobile to desktop with the menu open.

Pattern 3 — Fluid typography

🎨 CSS — font size that grows with viewport
/* Approach 1: stepped — breakpoints change font size */ html { font-size: 16px; } @media (min-width: 768px) { html { font-size: 17px; } } @media (min-width: 1280px) { html { font-size: 18px; } } /* Approach 2: fluid — clamp() (covered in Chapter 8) */ html { font-size: clamp(16px, 1.2vw + 12px, 20px); /* minimum 16px, grows with viewport, maximum 20px */ } /* Approach 3: Tailwind-style — use modular scale per breakpoint */ h1 { font-size: 1.75rem; } @media (min-width: 768px) { h1 { font-size: 2.5rem; } }

Pattern 4 — Responsive images

🎨 CSS — preventing images from breaking layouts
/* The universal image reset */ img { max-width: 100%; /* never wider than its container */ height: auto; /* maintain aspect ratio */ display: block; /* eliminate inline-block gap below image */ } /* Hide/show different art direction at breakpoints */ .hero-img-mobile { display: block; } .hero-img-desktop { display: none; } @media (min-width: 768px) { .hero-img-mobile { display: none; } .hero-img-desktop { display: block; } } /* Better: use the HTML <picture> element for art direction CSS cannot change which image src the browser downloads. <picture> tells the browser to pick the right source. */
Always add max-width: 100%; height: auto to all images as a global reset — it is one of the most important two lines in any stylesheet. Without it, a 1200px-wide image renders at 1200px regardless of its container, causing horizontal overflow.

10 — Quick Reference

Key media features

FeatureTestsCommon values
width / min-width / max-widthViewport widthpx, rem, em
height / min-height / max-heightViewport heightpx, rem
orientationPortrait vs landscapeportrait · landscape
prefers-color-schemeOS colour modedark · light
prefers-reduced-motionReduce motion settingreduce · no-preference
hoverPrimary input can hoverhover · none
pointerPointer precisionfine · coarse · none
prefers-contrastContrast preferencemore · less · no-preference

Logical operators

OperatorMeaningExample
andAll conditions true(min-width: 600px) and (orientation: landscape)
, (comma)Any condition true (OR)(max-width: 480px), (orientation: portrait)
notNegates the querynot (hover: hover)

Old vs new range syntax

Old (Level 3)New (Level 4)
(min-width: 600px)(width >= 600px)
(max-width: 959px)(width < 960px)
(min-width: 600px) and (max-width: 959px)(600px <= width < 960px)

✏️ Exercises

Write the CSS from scratch before checking the solution. Test in a browser by resizing the window — DevTools Responsive Mode (F12 → Toggle device toolbar) makes this easy.

Exercise 1
Build a mobile-first card grid. Base (mobile): single column, 16px gap, cards with 20px padding. At 600px: two columns. At 960px: three columns, 24px gap. At 1280px: four columns. Use min-width queries throughout. Include the viewport meta tag.
Hint: Start with grid-template-columns: 1fr as the base, then progressively add columns inside each min-width breakpoint. Only the column count and gap need to change — padding stays the same.
HTML
<meta name="viewport" content="width=device-width, initial-scale=1">
CSS
.card-grid { display: grid; grid-template-columns: 1fr; gap: 16px; } .card { padding: 20px; } @media (min-width: 600px) { .card-grid { grid-template-columns: repeat(2, 1fr); } } @media (min-width: 960px) { .card-grid { grid-template-columns: repeat(3, 1fr); gap: 24px; } } @media (min-width: 1280px) { .card-grid { grid-template-columns: repeat(4, 1fr); } }
Exercise 2
Add a dark mode to a simple page. Define CSS custom properties for --bg, --surface, --text, and --accent in light mode on :root. Then write a prefers-color-scheme: dark query that redefines all four variables. Apply the variables to body, a .card component, and all a link elements.
Hint: Only the variable definitions need to go inside the media query — any rule that uses var() updates automatically. You don't need to repeat the full rules inside the query.
CSS
:root { --bg: #ffffff; --surface: #f3f4f6; --text: #111827; --accent: #4f8ef7; } @media (prefers-color-scheme: dark) { :root { --bg: #0d0f18; --surface: #1a1d27; --text: #e2e4ec; --accent: #7ab8ff; } } body { background: var(--bg); color: var(--text); } .card { background: var(--surface); color: var(--text); } a { color: var(--accent); }
Exercise 3
Write a .hero section with a large animated background gradient. Add a prefers-reduced-motion query that disables the animation entirely and substitutes a static background for users who have enabled Reduce Motion. Then add a hover: none query that removes a :hover scale transform from a .hero-btn inside the hero (touch devices cannot hover).
Hint: Use animation: none inside the reduced-motion query to cancel any named keyframe animation. Use pointer-events: auto but remove the transform in the hover-none query.
CSS
@keyframes gradientShift { 0% { background-position: 0% 50%; } 100% { background-position: 100% 50%; } } .hero { background: linear-gradient(135deg, #1a2a4a, #2a1a4a, #1a3a2a); background-size: 300% 300%; animation: gradientShift 6s ease infinite alternate; } .hero-btn { transition: transform 0.2s; } .hero-btn:hover { transform: scale(1.05); } @media (prefers-reduced-motion: reduce) { .hero { animation: none; background-size: 100% 100%; /* static gradient */ } } @media (hover: none) { .hero-btn:hover { transform: none; } }
Exercise 4
Rewrite these three desktop-first media queries as mobile-first equivalents (using min-width) that produce the same visual result. Then rewrite the range query using the Level 4 range syntax.

/* Desktop-first */
.grid { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 1023px) { .grid { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 767px) { .grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 479px) { .grid { grid-template-columns: 1fr; } }
Hint: In mobile-first, the base style is the mobile (1 column) version. Each min-width breakpoint adds a column. For the range syntax, target the tablet range using 600px <= width < 960px.
CSS — mobile-first conversion
/* Mobile-first — base is mobile */ .grid { grid-template-columns: 1fr; } @media (min-width: 480px) { .grid { grid-template-columns: repeat(2, 1fr); } } @media (min-width: 768px) { .grid { grid-template-columns: repeat(3, 1fr); } } @media (min-width: 1024px) { .grid { grid-template-columns: repeat(4, 1fr); } } /* Level 4 range syntax — targeting the tablet range */ @media (768px <= width < 1024px) { .grid { grid-template-columns: repeat(3, 1fr); } }