# Responsive web design

Source: https://codewiki.com/frontend/responsive-design/

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

<!-- quick -->

```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>
```

```text
680px: 2 columns
```

<!-- /quick -->

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.

```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>
```

```text
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.

```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>
```

```text
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.

```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>
```

```text
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

> **Pitfall:** Labeling `768px` as the “tablet breakpoint” and assuming touch on one side and a mouse on the other confuses device categories with layout constraints. A split desktop window can be narrow, and a tablet can have a precise pointer.
>
> **Fix:** derive breakpoints from real content failures. Test input with `hover`, `pointer`, and a keyboard separately instead of inferring capabilities from viewport width.

> **Pitfall:** `width: 100vw` ignores an element's container and may include the vertical scrollbar in its box. `height: 100vh` can also hide content as mobile browser chrome expands and collapses.
>
> **Fix:** prefer `width: auto` or `100%` for ordinary blocks. When something truly fills viewport height, choose the stable small viewport unit `svh` or the chrome-responsive `dvh` deliberately, and keep content scrollable.

> **Pitfall:** A Flex or Grid item can overflow even when its track uses `1fr`, because a long URL, table, or preformatted block contributes a large automatic minimum size.
>
> **Fix:** use `minmax(0, 1fr)` for tracks that should shrink and set `min-width: 0` on the item when necessary. Give real long content an explicit wrapping or local-scrolling policy as well.

> **Pitfall:** Using `order`, grid coordinates, or `grid-auto-flow: dense` to create an attractive narrow layout can make visual order diverge from DOM reading order and sequential keyboard focus.
>
> **Fix:** write the DOM in a meaningful reading order first. Let layout switches change geometry only. If the task order truly differs, revisit the document structure instead of masking it with CSS.

> **Pitfall:** A `srcset` can list several image candidates while `sizes` still claims the image is always `100vw`. The browser may then choose a resource much wider than the actual column, and shrinking it with CSS does not undo that request.
>
> **Fix:** keep `sizes` synchronized with final layout conditions, then inspect network requests and rendered size on both sides of each breakpoint. Use `picture` for different compositions and same-content candidates for resolution choices.

<!-- deep -->

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

<!-- /deep -->

[Checkpoint: frontend/responsive-design](https://codewiki.com/frontend/responsive-design/#checkpoint)

## Further reading

- [Media Queries Level 5](https://www.w3.org/TR/mediaqueries-5/)
- [CSS Containment Module Level 3](https://www.w3.org/TR/css-contain-3/)
- [HTML Standard: Responsive images](https://html.spec.whatwg.org/multipage/images.html#responsive-images)
- [MDN: Responsive design](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/CSS_layout/Responsive_Design)
- [WCAG 2.2: Understanding Reflow](https://www.w3.org/WAI/WCAG22/Understanding/reflow.html)
