Responsive design

Build a page that works at every width without a stack of breakpoints.

15of 26
≈ 13Min read
9Parts

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

without this, a phone renders at 980px and shrinks
<meta name="viewport" content="width=device-width, initial-scale=1">
and images that never exceed their box
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.

four rules that replace a stylesheet of breakpoints
/* 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 rem in the query, not px: 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).
mobile first: add, do not undo
.layout {
  display: grid;
  gap: 2rem;
}

@media (min-width: 60rem) {
  .layout {
    grid-template-columns: 16rem minmax(0, 1fr);
  }
}

Queries that are not about width

ask about the device, not the pixels
@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; }
}
Hover on a touch screen

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%.

a scale that moves with the viewport
: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.

the physical details
.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 width or height on anything containing text.
  • width: 100vw on 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 :hover only.
  • user-scalable=no in the viewport meta.

Practice