Responsive web design

Adapt pages and components to available space, user preferences, and input capabilities without overflow, visual reordering, or wrong resource choices.

level intermediate time 10 min at Standard depth
version Media Queries Level 5 / CSS Containment Level 3
what

Responsive web design changes how one body of content is presented in response to available space, user preferences, and input capabilities.

trap

Adding media queries at a few device widths misses constraints caused by component containers, zoom, long text, and non-mouse input.

fix

Start with a layout that can shrink and wrap, enhance it with content-driven media or container queries, and test real content on both sides of each boundary.

What it is and why it exists

Responsive design does not mean maintaining separate pages for phones, tablets, and desktops. It means keeping one document readable and operable as its constraints change. Those constraints include container width, text zoom, orientation, pointer capability, and motion preferences as well as viewport width.

It solves the unknown-display problem. You cannot enumerate every window size, split-screen ratio, embedding context, translation length, and system setting during development. A layout tied to a few assumed device sizes therefore overflows or hides content in the states between them. Responsive rules turn those assumptions into conditions a browser can evaluate.

A typical implementation combines fluid sizes, layouts that wrap or rearrange, flexible media, and conditional rules. CSS Grid, Flexbox, min(), max(), and clamp() can adapt continuously without a breakpoint. A media query responds to the viewport or user environment; a container query responds to the space allocated to a reusable component.

Responsive also does not mean making everything smaller. Navigation can change from a row to a disclosure, a data table may need a different information structure, and an image can use a different resource candidate. The document order, accessible names, and task path should remain coherent throughout those changes.

How it works

The browser computes base styles through the normal cascade, then evaluates conditional rules. When a condition changes, such as a narrowing window or a component entering a narrower column, it recalculates the affected styles and layout. JavaScript usually does not need to duplicate that decision.

Base rules should cover the most constrained available space and preserve normal flow. You then add capability with min-width or range queries. This is progressive enhancement : if a condition does not match or an enhancement is unavailable, the core content still exists.

Fluid layout reduces the number of breakpoints first. max-width caps a comfortable line length, minmax() gives grid tracks a usable lower bound, flex-wrap lets items form new lines, and clamp() gives type or spacing a minimum, fluid preference, and maximum. Fixed pixels can still express borders or a validated minimum size, but they should not encode an assumed page width.

CSS
.page {
  width: min(100% - 2rem, 70rem);
  margin-inline: auto;
}

.cards {
  display: grid;
  gap: clamp(0.75rem, 2vw, 1.5rem);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
}

.cards > * {
  min-width: 0;
}

.hero-title {
  font-size: clamp(2rem, 1.4rem + 3vw, 4rem);
}

img,
video {
  display: block;
  max-width: 100%;
  height: auto;
}

These base rules do not guess a device type. The page has a cap and gutters, cards form columns from their own lower bound, the heading scales continuously between explicit bounds, and media cannot exceed its content box.

A breakpoint is where a layout constraint needs a discrete change, not a nickname for a device. Stretch the viewport slowly and add a breakpoint where content becomes crowded, line length loses control, or controls collide. Use em or rem for conditions tied to text scale so a changed root size does not leave layout locked to the old pixel assumption.

Media queries observe the viewport or user environment. Size queries can change page-level structure, prefers-reduced-motion can select a reduced-motion alternative, and hover and pointer describe input capabilities. The last two do not reliably identify a phone or desktop, and a hybrid device can expose several capabilities.

Container queries observe an ancestor query container instead of the viewport. After container-type: inline-size, a descendant can switch layout with @container (width >= 30rem) based on the container’s inline size. The same card can therefore be horizontal in a main column and vertical in a sidebar without knowing the page template.

CSS
.card-host {
  container: card / inline-size;
}

@media (width >= 64rem) {
  .page-shell {
    grid-template-columns: minmax(0, 1fr) 18rem;
  }
}

@container card (width >= 30rem) {
  .card {
    grid-template-columns: 10rem minmax(0, 1fr);
  }
}

@media (hover: hover) and (pointer: fine) {
  .menu-button:hover {
    text-decoration: underline;
  }
}

@media (prefers-reduced-motion: reduce) {
  .panel {
    scroll-behavior: auto;
  }
}

These conditions read the viewport, a query container, primary input capabilities, and a user preference respectively. They answer different questions and should not collapse into one “mobile” Boolean.

Responsive images address resource selection and art direction. With width descriptors in srcset, sizes tells the browser the image’s expected CSS width under each condition, and the browser combines that with device pixel ratio to choose a candidate. picture can select a different crop or format; it does not replace accurate alt text or explicit intrinsic dimensions.

Examples

An intrinsic grid without breakpoints

First, let the content and available space determine the column count. min() prevents the minimum track from overflowing when the container itself is narrower than 14rem, and the long title can break inside its card.

intrinsic-grid.html
<section class="gallery" aria-label="Plans">
  <article class="card"><h2>Starter</h2><p>For personal projects.</p></article>
  <article class="card"><h2>Collaboration</h2><p>For growing teams.</p></article>
  <article class="card"><h2>EnterpriseSecurityControls</h2><p>For regulated work.</p></article>
</section>

<style>
  .gallery {
    display: grid;
    width: 680px;
    gap: 16px;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
  }

  .card {
    min-width: 0;
    padding: 16px;
    border: 1px solid #888;
  }

  .card h2 {
    overflow-wrap: anywhere;
  }
</style>

<script>
  const gallery = document.querySelector('.gallery');
  const tracks = getComputedStyle(gallery).gridTemplateColumns.split(' ').length;
  console.log(`${gallery.clientWidth}px: ${tracks} columns`);
</script>
680px: 2 columns

That output comes from a real Chromium layout with a 680px-wide gallery. Three 14rem tracks plus two gaps do not fit, so Grid creates two columns and distributes the remaining space between them. No device breakpoint is involved.

A page switch triggered by content

Add a media query when the page structure truly needs a discrete change. This example stays in one column at narrow viewports and splits only when both the report and supporting column fit.

media-query.html
<meta name="viewport" content="width=device-width">
<main class="layout">
  <article>Main report</article>
  <aside>Filters</aside>
</main>

<style>
  .layout {
    display: grid;
    gap: 1rem;
    grid-template-columns: minmax(0, 1fr);
  }

  .layout > * {
    min-width: 0;
    padding: 1rem;
    border: 1px solid #888;
  }

  @media (width >= 48rem) {
    .layout {
      grid-template-columns: minmax(0, 1fr) 16rem;
    }
  }
</style>

<script>
  const columns = getComputedStyle(document.querySelector('.layout'))
    .gridTemplateColumns.split(' ').length;
  console.log(`640px viewport: ${columns === 1 ? 'stacked' : 'split'}`);
</script>
640px viewport: stacked

The output was captured in a 640px CSS viewport. At the default root font size, 48rem is 768px, so the enhancement does not match. The report still precedes the supporting content in the DOM.

Reusing a component by container

The card does not know whether it appears on a full page, in a main column, or in a sidebar. It reads the nearest named query container. Because the breakpoint belongs to the component’s content constraint, moving the component to another page template does not require synchronizing global viewport breakpoints.

container-card.html
<section class="card-host">
  <article class="profile">
    <div class="avatar" aria-hidden="true">AL</div>
    <div><h2>Ada Lovelace</h2><p>Computing notes and correspondence</p></div>
  </article>
</section>

<style>
  .card-host {
    container: profile / inline-size;
    width: 520px;
  }

  .profile {
    display: grid;
    grid-template-columns: 1fr;
    gap: 1rem;
    padding: 1rem;
    border: 1px solid #888;
  }

  .avatar {
    display: grid;
    place-items: center;
    min-block-size: 8rem;
    background: #ddd;
  }

  @container profile (width >= 30rem) {
    .profile { grid-template-columns: 8rem minmax(0, 1fr); }
  }
</style>

<script>
  const columns = getComputedStyle(document.querySelector('.profile'))
    .gridTemplateColumns.split(' ').length;
  console.log(`520px container: ${columns} columns`);
</script>
520px container: 2 columns

At the default root font size, the 30rem threshold is 480px, so the 520px container matches the horizontal layout. The query examines .card-host. It cannot query and also depend on .profile’s own query-driven size, which could create an unstable feedback loop.

Separate preferences from device size

Motion preference is independent of viewport width. In the page below, emulating reduced motion removes the transition while a precise pointer can still expose a hover hint. The core button exists under every combination.

capabilities.html
<button class="save-button" type="button">
  Save <span class="hint">(Ctrl+S)</span>
</button>

<style>
  .save-button {
    padding: 0.75rem 1rem;
  }

  .hint {
    display: none;
  }

  @media (hover: hover) and (pointer: fine) {
    .save-button:hover .hint {
      display: inline;
    }
  }

  @media (prefers-reduced-motion: no-preference) {
    .save-button {
      transition: transform 160ms ease-out;
    }

    .save-button:active {
      transform: scale(0.98);
    }
  }
</style>

<script>
  const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
  const precise = matchMedia('(hover: hover) and (pointer: fine)').matches;
  console.log(`reduced motion: ${reduced}; precise pointer: ${precise}`);
</script>
reduced motion: true; precise pointer: false

The output comes from headless Chromium with reduced motion enabled and no precise primary pointer. The queries are evaluated independently, so code must not infer a user’s preference or input method from screen width.

Pitfalls

Deep Breakpoints, containers, and source order

Breakpoints, containers, and source order

Choose the query coordinate system first

Use a media query when the condition describes the overall browsing environment. Page navigation, a viewport-level two-column structure, print styles, and user preferences belong here. A component should not guess its allocated space from the global page width.

Use a container query when the condition describes space allocated to a component. The wrapper that owns that space should establish the query container, and a descendant responds to it. Do not scatter container-type across every element merely to make it queryable, because it changes sizing containment and which ancestor a query can select.

Use neither when the design does not need a discrete state. Grid auto-fit, Flexbox wrapping, percentages, minmax(), and clamp() express continuous constraints and reduce rule conflicts at boundaries. Queries handle layout-mode changes; fluid values handle adjustment within a mode.

Derive breakpoints from failures

Complete the base layout with real content at the narrowest supported width, then grow the container. Record where a heading becomes too long, controls gain wasteful empty space, or two content groups finally fit beside each other. Put the breakpoint before the failure instead of rounding it to the nearest device preset.

Cascade errors tend to appear around a breakpoint. Test at least one CSS pixel below and above the threshold, then change the root font size. If a query uses rem or em, changing text scale intentionally changes its pixel threshold. Range syntax such as @media (40rem <= width < 64rem) expresses a half-open interval directly and avoids adjacent min-width and max-width rules overlapping at one point.

Container breakpoints also come from component content. Change the actual host width while testing a card instead of dragging only the top-level viewport. A fixed sidebar can give a card little space in a wide viewport, which is exactly when container and media queries produce different answers.

Keep reading order in the document

CSS can change visual position, but screen readers and sequential keyboard navigation normally keep following the DOM. Write a source order that remains coherent in every mode, such as heading, body, primary action, and supporting information. One-column and multi-column layouts then become different geometries for the same narrative.

If a wide design places a sidebar earlier visually, do not make keyboard users first traverse controls drawn at the far end of the page. For interactive content, record DOM index, visual coordinate, and Tab index item by item. That comparison exposes mismatch more reliably than screenshots alone.

Build a constraint test matrix

Size tests should cover the boundaries and the space between them. Select cases that trigger real constraints: shortest and longest content, empty states, error messages, zoomed text, narrow containers, both orientations, and widths produced by embedding the page in a sidebar or dialog.

Capability tests are a separate axis from size tests. Complete the core task with a keyboard, simulate both a coarse pointer without hover and a precise pointer with hover, and enable reduced motion. Do not use a viewport preset as a substitute for any of those capabilities.

The WCAG 2.2 reflow guidance gives one explicit review target: except for content that requires two-dimensional layout, content should not require two-axis scrolling at the equivalent of 320 CSS pixels wide. Also identify the exact overflowing element. Adding overflow-x: hidden to the whole page merely hides evidence and potentially operable content.

Further reading

checkpoint

4 questions · 1 predict-the-output · 1 spot-the-bug

next up CSS flexbox CSS grid Accessibility soon Image optimization soon
Copy as Markdown Interview bank Edit on GitHub Report an error Was this clear?