Responsive — Cheat Sheet
HTML, CSS & Responsive · 4 topics. Download the PDF or the Instagram carousel and share it.
Media Queries, Breakpoints and Container Queries
Write mobile-first, choose breakpoints from where your content breaks rather than from device names, and use container queries when a component should respond to its own space.
- ✓Mobile-first with min-width means the smallest device parses the least CSS and overrides nothing
- ✓Breakpoints in rem respect the user's font size; breakpoints chosen from content beat device-named ones
- ✓Container queries let a component respond to its own width, which a media query cannot express
- ✓container-type creates a containment context, so apply it to a wrapper rather than the styled component
- ✓prefers-reduced-motion, hover and pointer queries matter as much as width — a phone has no hover and needs 44px targets
/* Base: the smallest screen, no query needed */
.grid { display: grid; gap: 1rem; grid-template-columns: 1fr }
/* Then add complexity upwards */
@media (min-width: 48rem) { /* 768px */
.grid { grid-template-columns: repeat(2, 1fr) }
}
@media (min-width: 64rem) { /* 1024px */
.grid { grid-template-columns: repeat(3, 1fr) }
}
/* Desktop-first (max-width) means a phone parses every desktop rule
and then overrides it — more CSS to override, and the smallest
device does the most work. */
/* Use rem in the query, not px: the query then respects the user's
font size, so a zoomed-in user gets the simpler layout that fits.
/* Modern range syntax, where supported */
@media (width >= 48rem) { }
@media (48rem <= width <= 64rem) { }
/* Breakpoints from content, not from device names. There is no
"iPhone width" worth targeting — there are dozens. Common
starting values, to be adjusted once you see your content:
40rem (640) 48rem (768) 64rem (1024) 80rem (1280) */Fluid Type and Spacing
Scaling type and space continuously with clamp() replaces most sizing breakpoints and removes the jumps where a heading changes size mid-drag.
- ✓clamp() interpolates a value continuously, removing the jumps that breakpoint-based sizing produces
- ✓A preferred value in vw alone ignores the user's font-size setting — always include a rem term
- ✓Fluid spacing tokens fix both cramped phone padding and absurd desktop padding without media queries
- ✓width: min(100% - 2rem, 72rem) with margin-inline: auto is the complete content wrapper in two lines
- ✓Intrinsic patterns like auto-fit grids and flex-basis wrapping respond to the container, leaving media queries for real structural changes
/* clamp(minimum, preferred, maximum) */
h1 { font-size: clamp(2rem, 1.5rem + 2.5vw, 3.5rem) }
p { font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem) }
/* Why the preferred value is a SUM:
clamp(2rem, 5vw, 3.5rem) — pure vw: a user who doubles
their browser font size sees
no change at all
clamp(2rem, 1.5rem + 2.5vw, 3.5rem) — the rem term keeps it
responsive to that setting */
/* Deriving the numbers: to go from 2rem at 320px to 3.5rem at 1280px
slope = (3.5 - 2) / (1280 - 320) * 100vw = 0.156vw per px = 15.6vw... */
/* In practice use a generator (utopia.fyi) rather than the algebra,
and check both ends in the browser. */
/* A fluid scale, defined once as tokens */
:root {
--step--1: clamp(0.83rem, 0.8rem + 0.15vw, 0.94rem);
--step-0: clamp(1rem, 0.96rem + 0.2vw, 1.13rem);
--step-1: clamp(1.2rem, 1.14rem + 0.3vw, 1.41rem);
--step-2: clamp(1.44rem, 1.35rem + 0.45vw, 1.76rem);
}
h2 { font-size: var(--step-2) }Responsive Images and Media
Images are usually the largest thing on a page. srcset, modern formats and a reserved aspect ratio decide both how fast the page loads and whether it jumps while loading.
- ✓srcset lists the available files and sizes describes the rendered width — omitting sizes makes the browser assume 100vw
- ✓picture with AVIF and WebP sources roughly halves image bytes with a JPEG fallback
- ✓width and height attributes let the browser reserve space and are the main defence against layout shift
- ✓aspect-ratio with object-fit: cover handles images whose dimensions are not known in advance
- ✓Never lazy-load the hero image — it is the LCP element and should be preloaded with high fetch priority
<!-- Density switching: same size, different resolutions -->
<img src="logo.png" srcset="logo.png 1x, logo@2x.png 2x" alt="AiCanCode">
<!-- Width switching: different sizes for different layouts -->
<img
src="hero-800.jpg"
srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1600.jpg 1600w"
sizes="(min-width: 64rem) 50vw, 100vw"
width="1600" height="900"
alt="…">
<!-- srcset says how WIDE each file is.
sizes says how wide the IMAGE will be at each breakpoint.
The browser combines them with the device pixel ratio and picks.
Omitting sizes makes the browser assume 100vw and download a
file that is often twice as large as needed. -->
<!-- Format negotiation: AVIF, then WebP, then a fallback -->
<picture>
<source type="image/avif" srcset="hero.avif">
<source type="image/webp" srcset="hero.webp">
<img src="hero.jpg" alt="…" width="1600" height="900">
</picture>
<!-- Art direction: a genuinely different crop on a phone -->
<picture>
<source media="(max-width: 40rem)" srcset="hero-square.jpg">
<img src="hero-wide.jpg" alt="…">
</picture>Responsive Patterns and Testing
Navigation, tables and forms are where responsive designs actually break. Each has a known set of solutions, and each needs testing on a real device rather than a resized window.
- ✓A navigation toggle needs aria-expanded, aria-controls, focus management and Escape — the CSS is the easy half
- ✓Inputs below 16px make iOS zoom the page; fix the font size rather than blocking zoom
- ✓user-scalable=no is a WCAG violation — never disable pinch zoom
- ✓Touch targets need roughly 44px and spacing between them, and primary actions belong within thumb reach
- ✓Throttle CPU and network, test at 200% zoom, and verify on a real mid-range Android — DevTools cannot show the on-screen keyboard or true scroll performance
/* Base: the phone. A toggle and a hidden panel. */
.nav-toggle { display: block }
.nav-list { display: none }
.nav-list[data-open] { display: flex; flex-direction: column }
@media (min-width: 48rem) {
.nav-toggle { display: none }
.nav-list { display: flex; flex-direction: row; gap: 1.5rem }
}
<!-- The toggle must announce its state, and control the panel -->
<button class="nav-toggle" aria-expanded="false" aria-controls="nav-list">
<span class="sr-only">Menu</span>
<svg aria-hidden="true">…</svg>
</button>
<ul id="nav-list">…</ul>
/* When the drawer is open: trap focus inside it, close on Escape,
return focus to the toggle, and lock body scroll. A drawer that
leaves focus behind it is a keyboard trap in reverse — the user
tabs into a menu they cannot see. */
/* Alternatives to a hamburger, both often better:
- a visible row that scrolls horizontally
- the priority+ pattern: show what fits, "More" for the rest
A hamburger hides navigation, which measurably reduces its use. */