# Responsive design

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

Frontend Developer roadmap · The web platform · 16 min read · https://levelcode.org/roadmaps/frontend/responsive-design

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

| Unit | Relative to | Good for |
| --- | --- | --- |
| `%` | The parent's size | Widths inside a container |
| `rem` | The root element's font size (16px by default) | Font sizes, spacing, breakpoints |
| `em` | The current element's font size | Padding that should scale with a button's text |
| `vw` / `vh` | 1% of the viewport's width / height | Full-bleed sections, fluid type (combined with `rem`) |
| `dvh` / `svh` / `lvh` | Dynamic / smallest / largest viewport height | Full-height sections on mobile |
| `ch` | The width of the "0" character | Readable 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.

> [!WARNING]
> Never size text in `vw` alone, such as `font-size: 4vw`. Viewport units do not change when the user zooms, so the text cannot be enlarged – an accessibility failure. Always mix in a `rem` term, as in the example above.

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

> [!TIP]
> A good rule of thumb: media queries for **page layout** (how many columns, where the sidebar goes), container queries for **components** (how a card, form or widget arranges itself in the space it gets).

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

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

## Quiz

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

   A. A max-width media query
   B. container-type on the body
   C. The viewport meta tag
   D. A srcset attribute on images

   **Answer: C.** Without the viewport meta tag, mobile browsers lay the page out at around 980px and shrink it, so width-based media queries see a desktop-sized viewport.

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

   A. vw units are not supported on mobile
   B. clamp() loads faster
   C. 3vw is always smaller than 1rem
   D. The rem parts respond to user zoom and font settings, and the size is bounded

   **Answer: D.** Pure vw text ignores zoom and can become tiny or huge. Mixing rem in keeps it zoomable, and clamp keeps it within sensible minimum and maximum sizes.

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

   A. No ancestor of .card has container-type set
   B. Container queries only work with px units
   C. The rule must use max-width
   D. Container queries require JavaScript

   **Answer: A.** A container query looks for the nearest ancestor that is a query container. Setting container-type: inline-size on a wrapper fixes it; an element cannot query its own size.

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

   A. With max-width media queries that undo the mobile styles
   B. With min-width media queries that build on the base styles
   C. With a separate stylesheet for desktop only
   D. With JavaScript that checks window.innerWidth

   **Answer: B.** Mobile-first base styles target small screens, and min-width queries layer on extra layout as space grows. max-width queries that undo styles are the desktop-first approach.
