Start with the thing that already works
An unstyled HTML page is responsive. Text wraps, blocks fill the width, nothing overflows. Everything that breaks responsiveness is something you added: a fixed width, a fixed height, an absolute position, an image with no limit.
So the job is less "make it responsive" than "do not break it, and let it flex where it needs to".
The two lines that come first
<meta name="viewport" content="width=device-width, initial-scale=1">
img, video, svg, canvas {
max-width: 100%;
height: auto;
display: block;
}
Intrinsic before breakpoints
Most layouts do not need a media query at all. Ask the layout to adapt to the space it has, and it will handle widths you never tested.
/* a wrapper with a gutter that never disappears */
.wrap { width: min(70rem, 100% - 3rem); margin-inline: auto; }
/* cards that reflow on their own */
.deck { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: 1.25rem; }
/* type that scales between two bounds */
h1 { font-size: clamp(2rem, 1.2rem + 3vw, 4rem); }
/* a row that becomes a column when it runs out of room */
.row { display: flex; flex-wrap: wrap; gap: 1rem; }
.row > * { flex: 1 1 18rem; }
Media queries, when you do need them
- Write the small-screen rules as the default and add at larger widths. Undoing desktop rules on mobile means every phone downloads and applies CSS it does not need.
- Use
remin the query, notpx: the breakpoint then moves with the reader's font size. - Choose breakpoints where your layout breaks, not at device names. There is no 768px phone any more.
- Range syntax is clearer:
@media (40rem <= width <= 60rem).
.layout {
display: grid;
gap: 2rem;
}
@media (min-width: 60rem) {
.layout {
grid-template-columns: 16rem minmax(0, 1fr);
}
}
Queries that are not about width
@media (prefers-reduced-motion: reduce) { }
@media (prefers-color-scheme: dark) { }
@media (prefers-contrast: more) { }
@media (hover: hover) and (pointer: fine) {
.card:hover { transform: translateY(-3px); } /* only where hover exists */
}
@media (pointer: coarse) {
.btn { min-height: 44px; } /* a finger-sized target */
}
@media print {
nav, .sidebar { display: none; }
}
A phone has no hover. Without @media (hover: hover), a hover style fires on tap and then sticks until you tap elsewhere. Anything that only appears on hover is invisible on touch — so never hide something important behind one.
Fluid type
Every middle value has a rem term. A pure vw value does not respond to zoom, which fails the requirement that text can be enlarged to 200%.
:root {
--step-0: clamp(1rem, 0.96rem + 0.2vw, 1.125rem);
--step-1: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem);
--step-2: clamp(1.5rem, 1.3rem + 1vw, 2rem);
--step-3: clamp(2rem, 1.6rem + 2vw, 3rem);
--step-4: clamp(2.5rem, 1.8rem + 3.5vw, 4.5rem);
}
Touch targets and safe areas
44 × 44 CSS pixels is the accepted minimum for a touch target. It applies to the tappable area, not the visible icon — padding counts.
.btn {
min-height: 44px;
min-width: 44px;
}
/* the notch and the home indicator */
.app-bar {
padding-bottom: max(1rem, env(safe-area-inset-bottom));
}
Testing
- Drag the window slowly from 320px to 2000px and watch for the width where something breaks. That is your breakpoint.
- 320px is the narrowest you need to support; below that, horizontal scrolling is acceptable.
- Zoom to 200% and 400%. At 400% a desktop layout must reflow like a phone — no horizontal scrolling.
- Check with the longest real content you have, not the placeholder.
- Check in the device toolbar and by resizing — they are not the same thing.
The list of things that break it
- Fixed
widthorheighton anything containing text. width: 100vwon a scrolling page.- A flex or grid child without
min-width: 0. - Absolute positioning where grid would do.
- Long unbroken strings with no
overflow-wrap. - Content hidden behind
:hoveronly. user-scalable=noin the viewport meta.