Step 05 of 21

Responsive design

Make layouts that adapt from phones to wide monitors using fluid units, media queries, container queries and responsive images.

16 min 9 sections

Your page will be viewed on a 360px-wide phone, a tablet turned sideways, a laptop, and a 2560px monitor – sometimes by the same person on the same day. Responsive design means building one page that works well on all of them, rather than separate "mobile" and "desktop" sites. This lesson starts with fluid foundations that adapt automatically, then adds media queries for page-level changes and container queries for components that adapt to wherever they are placed.

The viewport meta tag

When smartphones arrived, most sites were designed for desktops. So mobile browsers pretended to be a desktop: they laid the page out on a virtual canvas around 980px wide and shrank it to fit the screen. Text became unreadably small.

To opt out of that behaviour, every responsive page needs this tag in its <head>:

html
<meta name="viewport" content="width=device-width, initial-scale=1">

It tells the browser to make the layout viewport as wide as the device's screen in CSS pixels (around 360–430px on most phones) and not to zoom. Without it, none of your media queries will behave as expected on mobile. Never add maximum-scale=1 or user-scalable=no: they stop people zooming in to read.

Fluid foundations

The most robust responsive layouts need surprisingly few breakpoints, because they are built from things that stretch and shrink on their own.

Relative units

UnitRelative toGood for
%The parent's sizeWidths inside a container
remThe root element's font size (16px by default)Font sizes, spacing, breakpoints
emThe current element's font sizePadding that should scale with a button's text
vw / vh1% of the viewport's width / heightFull-bleed sections, fluid type (combined with rem)
dvh / svh / lvhDynamic / smallest / largest viewport heightFull-height sections on mobile
chThe width of the "0" characterReadable line lengths

Using rem for font sizes and spacing means your layout scales when a user increases their browser's default font size – a setting many people with low vision rely on.

Containers that never overflow

Fixed widths are the most common cause of horizontal scrolling on phones. Instead, cap a width and let it shrink:

css
.container {
  width: min(100% - 2rem, 70rem); /* 1rem gutter each side, max 70rem */
  margin-inline: auto;            /* centre it */
}

.prose {
  max-width: 65ch;                /* comfortable reading line length */
}

img, video {
  max-width: 100%;                /* never wider than their container */
  height: auto;                   /* keep the aspect ratio */
  display: block;
}

min() picks the smaller of its arguments, so the container is full width minus gutters on a phone and 70rem on a big screen – no media query needed.

Fluid typography with clamp()

clamp(MIN, PREFERRED, MAX) returns the preferred value but never goes below the minimum or above the maximum:

css
h1 {
  font-size: clamp(1.75rem, 1.2rem + 2.5vw, 3rem);
}

At a 320px viewport, 1.2rem + 2.5vw is 19.2px + 8px = 27.2px, so the minimum (28px) applies. At 1280px it would be 51.2px, so the maximum (48px) applies. In between, the heading grows smoothly.

Intrinsic layouts

Layout techniques from the previous lesson are responsive by design: flex-wrap: wrap moves items onto new lines when space runs out, and repeat(auto-fit, minmax(min(16rem, 100%), 1fr)) changes the number of grid columns automatically. Reach for these first, and use queries for the changes they cannot express.

Media queries

A media query applies CSS only when a condition about the device or viewport is true:

css
@media (min-width: 48rem) {
  .site-nav { flex-direction: row; }
}

You can combine conditions with and, list alternatives with a comma, and use the modern range syntax, which all evergreen browsers support:

css
@media (min-width: 48rem) and (max-width: 63.99rem) { /* tablets only */ }
@media (48rem <= width < 64rem)                      { /* range syntax: no .99 hack */ }
@media (orientation: landscape), (min-width: 64rem)  { /* either condition */ }

Mobile first

Write your base styles for the smallest screen, then use min-width queries to add complexity as space grows:

css
/* Base: one column, stacked nav – works everywhere */
.site-nav { display: flex; flex-direction: column; gap: 0.5rem; }
.layout   { display: grid; gap: 1.5rem; }

/* Medium screens and up */
@media (min-width: 48rem) {
  .site-nav { flex-direction: row; gap: 1.5rem; }
}

/* Large screens and up: add a sidebar */
@media (min-width: 64rem) {
  .layout { grid-template-columns: 16rem minmax(0, 1fr); }
}

Mobile-first CSS tends to be shorter: small screens get the simplest styles and never apply overrides they do not need, and each query only adds. Desktop-first CSS with max-width queries spends its effort undoing styles.

Choosing breakpoints

Do not pick breakpoints from a list of popular devices – there are thousands of screen sizes. Instead, start with the narrow layout, widen the browser slowly, and add a breakpoint where the design starts to look wrong: lines become too long, cards too stretched, the nav too cramped. Most sites need only two or three.

Write breakpoints in rem or em. In media queries these units are based on the browser's default font size, so if a user sets a larger default text size, your layout switches to the narrower design earlier – exactly what they need.

Beyond width: user preference features

Media queries can also respond to how a person uses their device:

css
/* Respect users who get motion sickness from animation */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Dark mode, following the operating system setting */
@media (prefers-color-scheme: dark) {
  :root { --bg: #0f172a; --text: #e2e8f0; }
}

/* Only show hover effects on devices that can actually hover */
@media (hover: hover) {
  .card:hover { transform: translateY(-2px); }
}

(pointer: coarse) matches touchscreens, where tap targets should be generously sized. Remember that screen width says nothing about input: a small laptop window still has a mouse, and a large tablet still has fingers.

Container queries

Media queries ask about the viewport. But a product card might appear in a wide main column on one page and a narrow sidebar on another, at the same viewport width. What the card really cares about is the width of the space it has been given. Container queries answer exactly that.

html
<div class="card-slot">
  <article class="product">
    <img src="/img/shoe.jpg" alt="Grey trail shoe">
    <div class="product-body">
      <h3>Ridge Runner 3</h3>
      <p>Our lightest waterproof shoe.</p>
    </div>
  </article>
</div>
css
.card-slot {
  container-type: inline-size;   /* make this element a query container */
}

.product {
  display: grid;
  gap: 1rem;
}

@container (min-width: 28rem) {
  .product {
    grid-template-columns: 10rem 1fr;  /* image beside text when there is room */
  }
}

Now the card stacks vertically in a narrow slot and switches to a side-by-side layout in a wide one, wherever it is used.

Things to know:

  • An element cannot query itself. The query looks up to the nearest ancestor container, which is why the example has a wrapper element. Styles inside @container apply to descendants of the container.
  • Name containers when they are nested: container: card / inline-size; then @container card (min-width: 28rem).
  • inline-size containment means the container's width cannot depend on its content. Put it on elements that get their width from their parent, such as block elements or grid cells. On a shrink-to-fit element (like a float or an inline-block) it can collapse to zero width.
  • Container query units: cqi is 1% of the container's inline size, so font-size: clamp(1rem, 4cqi, 1.5rem) scales text with the component.

Responsive images

max-width: 100% makes an image fit, but a phone still downloads the full desktop-sized file. srcset and sizes let the browser pick a suitable file:

html
<img
  src="/img/hero-800.jpg"
  srcset="/img/hero-400.jpg 400w, /img/hero-800.jpg 800w, /img/hero-1600.jpg 1600w"
  sizes="(min-width: 64rem) 50vw, 100vw"
  alt="Runner on a ridge at sunrise"
  width="1600" height="900">

srcset lists the files and their actual widths (w). sizes tells the browser how wide the image will be displayed: half the viewport on large screens, full width otherwise. The browser combines that with the screen's pixel density and chooses the smallest file that will look sharp.

When you need a different crop on small screens – not just a smaller file – use <picture>:

html
<picture>
  <source media="(min-width: 48rem)" srcset="/img/team-wide.jpg">
  <img src="/img/team-square.jpg" alt="The Ridgeway team outside the shop">
</picture>

Testing and common pitfalls

Use the device toolbar in Chrome DevTools or Responsive Design Mode in Firefox DevTools to drag the viewport through every width, not just a few device presets. Then check on a real phone, and zoom to 200% on desktop.

  • Horizontal scrolling almost always comes from a fixed width, a long unbreakable string, or an image without max-width. In DevTools, look for the element that extends past the right edge.
  • 100vh on mobile includes the area behind the browser's address bar, so content gets hidden. Use min-height: 100dvh.
  • Hover-only interactions – menus that open on hover – do not work on touchscreens. Make them open on click or tap too.
  • Hiding content on mobile with display: none often removes things phone users need. Rearrange instead.

Practice

Build a page with a reusable profile card and a responsive layout:

  1. A .layout with a <main> and an <aside>. Mobile first: stacked on small screens; from 64rem up, main on the left and a 20rem aside on the right.
  2. A profile card (avatar image, name, short bio) wrapped in a container. Place one copy in <main> and one in <aside>.
  3. The card stacks vertically by default and shows the avatar beside the text when its container is at least 26rem wide.
  4. The page title uses fluid type between 1.75rem and 3rem.
  5. Hover effects on the card only apply on devices that can hover.

Resize the window: at large widths the card in <main> should be side by side while the card in the narrow <aside> stays stacked.

Show solution
html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Responsive practice</title>
  <style>
    *, *::before, *::after { box-sizing: border-box; }
    body { margin: 0; font-family: system-ui, sans-serif; line-height: 1.5; }
    img { max-width: 100%; height: auto; display: block; }

    .container { width: min(100% - 2rem, 75rem); margin-inline: auto; }
    h1 { font-size: clamp(1.75rem, 1.2rem + 2.5vw, 3rem); }

    .layout { display: grid; gap: 1.5rem; }
    @media (min-width: 64rem) {
      .layout { grid-template-columns: minmax(0, 1fr) 20rem; }
    }

    .card-slot { container-type: inline-size; }
    .profile {
      display: grid;
      gap: 1rem;
      padding: 1rem;
      border: 1px solid #cbd5e1;
      border-radius: 12px;
      transition: box-shadow 0.2s;
    }
    .profile img { width: 6rem; border-radius: 50%; }

    @container (min-width: 26rem) {
      .profile { grid-template-columns: auto 1fr; align-items: center; }
    }

    @media (hover: hover) {
      .profile:hover { box-shadow: 0 4px 16px rgb(0 0 0 / 0.12); }
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>Our team</h1>
    <div class="layout">
      <main>
        <div class="card-slot">
          <article class="profile">
            <img src="avatar.jpg" alt="" width="192" height="192">
            <div>
              <h2>Amara Okafor</h2>
              <p>Leads our trail-running clinics and tests every shoe we sell.</p>
            </div>
          </article>
        </div>
      </main>
      <aside>
        <div class="card-slot">
          <article class="profile">
            <img src="avatar.jpg" alt="" width="192" height="192">
            <div>
              <h2>Leo Martins</h2>
              <p>Runs the repair workshop.</p>
            </div>
          </article>
        </div>
      </aside>
    </div>
  </div>
</body>
</html>

The avatars use alt="" because the name next to them already identifies the person. From 64rem up, the aside is 20rem wide, so its card container never reaches 26rem and the card stays stacked, while the main column is far wider and its card sits side by side. Below 64rem both cards are full width, so they switch together depending on the window width – each card only ever looks at its own container. Save any square image as avatar.jpg next to the HTML file.

Key takeaways

  • Every responsive page needs <meta name="viewport" content="width=device-width, initial-scale=1">.
  • Start fluid: relative units, min() containers, max-width: 100% images and clamp() type remove most breakpoints.
  • Write mobile-first CSS and add min-width media queries, in rem, where the content needs them.
  • Media features such as prefers-reduced-motion, prefers-color-scheme and hover respond to how people use their device.
  • Container queries let components adapt to the space they are given; the container must be an ancestor with container-type.
  • srcset and sizes serve appropriately sized images; <picture> handles different crops.

Check yourself

Quiz

0/4 correct · 4 left

  1. 1.A page looks tiny and zoomed out on phones, and none of its media queries seem to apply. What is most likely missing?

  2. 2.Why is font-size: clamp(1rem, 0.8rem + 1vw, 1.5rem) better than font-size: 3vw?

  3. 3.You add @container (min-width: 30rem) { .card { … } } but the card never changes. Which is the most likely cause?

  4. 4.In mobile-first CSS, how are styles for larger screens usually added?

Finished reading?

Mark it complete to fill in your roadmap.