Module 6: Responsive Design & Media Queries
Media Queries: Mobile-First Breakpoints and User Preferences
Why the viewport meta tag matters, how to write mobile-first queries, how to choose breakpoints from your content, and which non-width queries you should be using.
A media query applies a block of CSS only when a condition is true. The condition might be the width of the window, the orientation, whether the user prefers dark mode, or whether the page is being printed. The browser evaluates queries continuously, so resizing the window turns rules on and off in real time.
The viewport tag comes first#
Before any media query works on a phone, the HTML needs this line in the head:
<meta name="viewport" content="width=device-width, initial-scale=1">
Without it, mobile browsers assume the page was designed for desktop and lay it out in a virtual viewport around 980px wide, and then shrink the result to fit the screen. Your max-width: 600px query would never fire, because the browser thinks the viewport is 980px. If your mobile layout looks like a tiny desktop page, this tag is missing.
Syntax#
@media (min-width: 768px) {
.sidebar { display: block; }
}
@media (min-width: 768px) and (max-width: 1023px) { ... } /* both must be true */
@media (max-width: 480px), print { ... } /* comma means OR */
@media not print { ... }
There is also a range syntax that reads more naturally, supported in all current browsers:
@media (width >= 768px) { ... }
@media (600px <= width < 1024px) { ... }
Mobile-first#
Write the base styles for the smallest screen. Then add complexity as the screen gets bigger, using min-width.
/* base: phones */
.cards {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
/* tablets and up */
@media (min-width: 640px) {
.cards { grid-template-columns: repeat(2, 1fr); }
}
/* desktops and up */
@media (min-width: 1024px) {
.cards { grid-template-columns: repeat(4, 1fr); }
}
width: 0 ─────────── 640 ─────────── 1024 ──────────►
rules: [base]
[base + 640 rules]
[base + 640 + 1024 rules]
Each query adds on top of the previous ones. Nothing needs to be undone.
The desktop-first alternative, writing for large screens and using max-width to scale down, forces you to cancel and override styles at each step. It tends to produce long override chains. It also makes the slowest devices, which are often phones, parse and discard more CSS. Mobile-first is simpler to reason about, and your most constrained layout is the default.
Choosing breakpoints#
Do not pick breakpoints from a list of device sizes. Devices change every year. Pick them from your content.
- Open the page at 320px wide.
- Slowly widen the window.
- When the layout starts to look wrong, whether lines get too long, cards get stretched or gaps look empty, that is your breakpoint. Add a query there.
Common starting points, which you adjust:
| Name | Min width | Typical use |
|---|---|---|
| sm | 640px | large phones, small tablets |
| md | 768px | tablets in portrait |
| lg | 1024px | laptops |
| xl | 1280px | desktops |
You usually need only two or three. The more breakpoints you add, the more combinations you must test.
Overlap bug#
@media (max-width: 768px) { .nav { flex-direction: column; } }
@media (min-width: 768px) { .nav { flex-direction: row; } }
At exactly 768px, both are true. Use max-width: 767.98px, or better, pick one direction (min-width) and avoid mixing.
Use em or rem in queries with care#
In media queries, em and rem both refer to the browser's default font size (usually 16px), not the one in your CSS. A query like (min-width: 48em) means 768px for a default user, and a larger width for someone who raised their default font size. That behavior is a feature. The layout adapts to the user's text size, not just the pixel width.
Beyond width#
The most useful queries often have nothing to do with screen size.
/* Respect the user's OS theme */
@media (prefers-color-scheme: dark) {
:root { --bg: #0b1220; --text: #e6e9ef; }
}
/* Respect users who get dizzy from motion */
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
/* Only add hover effects on devices that can hover */
@media (hover: hover) and (pointer: fine) {
.card:hover { transform: translateY(-2px); }
}
/* Larger tap targets on touch devices */
@media (pointer: coarse) {
.btn { min-height: 44px; }
}
/* Print styles */
@media print {
nav, .ad { display: none; }
body { color: #000; background: #fff; }
}
/* High-density displays */
@media (min-resolution: 2dppx) {
.logo { background-image: url("logo@2x.png"); }
}
prefers-reduced-motion and hover are accessibility and usability features. A hover effect that does nothing on touch screens and sticks after a tap is a common irritation, and the hover: hover query avoids it.
Media queries versus container queries#
A media query measures the viewport. That is wrong for a component that might sit in a narrow sidebar on one page and a wide main area on another. The card cannot know its own container's width through a media query. Container queries solve that, and Module 9 covers them. Media queries remain the right tool for page-level layout changes and user preferences.
Organizing queries#
You can put queries next to the component they affect, which keeps related code together, or collect them at the bottom of the file. Co-locating tends to scale better. You will never repeat a breakpoint value more than a few times, and a build tool can merge them later.
Common mistakes#
- No viewport meta tag.
- Desktop-first with
max-widthpile-ups, where each query undoes the previous one. - Device-based breakpoints such as "iPhone 14 width" that are out of date next season.
- Overlapping ranges at the exact boundary.
- Testing only at breakpoint widths. Check every width in between, because users have arbitrary window sizes.
- Hiding content on mobile because it does not fit, rather than redesigning it.
- Hover-only interactions that do not work on touch.
Practice#
- Take a three-column layout and make it mobile-first: one column by default, two at 640px, three at 1024px. Remove the viewport meta tag, test it in the mobile emulator, and note what breaks.
- Pick a page you have built and find its real breakpoints by dragging the window from 320px upward. Write down where it first looks wrong.
- Add a dark theme using
prefers-color-scheme, and a reduced-motion rule that disables your transitions. - Write a print stylesheet that hides navigation, shows link URLs after links using
a::after { content: " (" attr(href) ")"; }, and sets black text on white.