# Responsive Design

> How to build a layout that works from a phone screen to a desktop monitor: the viewport meta tag, media queries, fluid units, and responsive images.


---

# Responsive Design

A page that looks right on your laptop can look broken on a phone: text too small to read, or so huge you
scroll sideways to finish a sentence. Responsive design is the set of techniques that make one HTML/CSS
codebase adapt to whatever screen loads it, instead of shipping a separate "mobile site."

This guide assumes you're comfortable with CSS, Flexbox, and Grid from [Flexbox and Grid](/guides/flexbox-and-grid).
The running example is a 3-column card layout - a common pattern for pricing tables, product grids, and
blog previews - taken from a fixed-width layout that breaks on a phone to a fluid one that doesn't.

## The phases

1. **[The Viewport and Media Queries](01-the-viewport-and-media-queries.md)** - the meta tag that stops
   mobile browsers from faking a desktop screen, and `@media` queries for applying CSS conditionally.
2. **[Fluid Layouts](02-fluid-layouts.md)** - relative units, wrapping Flexbox/Grid, and `clamp()` for
   typography and spacing that scale without extra breakpoints.
3. **[Responsive Images and Mobile-First Workflow](03-responsive-images-and-mobile-first-workflow.md)** -
   `srcset`/`sizes`, `<picture>` for art direction, and why writing mobile-first CSS tends to end up simpler.

By the end, the card layout works from a 320px phone to a wide desktop without a separate mobile site.


---

# The Viewport and Media Queries

Load a page without the viewport tag on a phone and you get a tiny, zoomed-out version of the desktop
layout - text you have to pinch to read. That single missing line is one of the most common beginner
bugs in web development, and it has nothing to do with your CSS.

## Why phones fake a wide screen

Phone screens are physically narrow, but most sites in the early 2010s were built for desktop widths
around 980px. If phone browsers rendered pages at the phone's actual pixel width, every one of those
sites would break - text would wrap after three words, three-column layouts would collapse into
unreadable slivers.

Mobile browsers solved this by lying. By default, a phone browser renders the page as if the screen were
980px wide, then shrinks the whole result down to fit the physical screen. You get a legible-looking
miniature of the desktop page, zoomed out. That's why unstyled or improperly configured pages look "zoomed
out" on a phone: the browser rendered a wide virtual canvas and shrank it, rather than laying out content
at the screen's real width.

## The fix: `<meta name="viewport">`

Add this to every page's `<head>`:

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

- `width=device-width` tells the browser to use the screen's actual CSS pixel width as the layout
  viewport, instead of the fake 980px canvas.
- `initial-scale=1` sets the initial zoom level to 1:1 - no zooming in or out on load.

Without this tag, every media query you write below is close to useless: the browser is still laying
out the page at 980px virtual width, so a `max-width: 600px` query never matches on a phone, no matter
how narrow the physical screen is. This tag is not optional for a responsive page - it's the
prerequisite that makes media queries mean anything on mobile.

## Media query syntax

A media query wraps CSS rules in a condition. The browser applies the rules only when the condition
matches:

```css
@media (max-width: 600px) {
  .card {
    width: 100%;
  }
}
```

This says: when the viewport is 600px wide or narrower, make `.card` full width. Outside a matching
query, the rule inside it doesn't apply at all.

You can query on `min-width`, `max-width`, or combine conditions:

```css
@media (min-width: 600px) and (max-width: 899px) {
  /* applies only between 600px and 899px */
}
```

`min-width` means "this wide or wider"; `max-width` means "this wide or narrower." Media queries also
support orientation (`orientation: landscape`) and other conditions, but width is what you'll use for
almost every layout decision.

## The running example: a 3-column card layout

Say you're building a pricing page with three cards side by side:

```html
<div class="cards">
  <div class="card">Basic</div>
  <div class="card">Pro</div>
  <div class="card">Team</div>
</div>
```

```css
.cards {
  display: flex;
  gap: 16px;
}

.card {
  flex: 1;
  padding: 24px;
  border: 1px solid #ddd;
}
```

Three flexible columns, evenly split. On a desktop this looks fine. On a 375px phone, three columns
squeezed into that width means each card is roughly 100px wide - the padding alone eats most of it, and
any real content wraps into an unreadable mess.

A media query fixes the narrow case:

```css
@media (max-width: 600px) {
  .cards {
    flex-direction: column;
  }
}
```

Below 600px, the cards stack vertically instead of squeezing into three columns. Above 600px, the
default row layout stands. This is the pattern you'll repeat constantly: define the normal layout, then
override it inside a media query for the width range where it breaks.

## Choosing breakpoints

Common breakpoint numbers show up everywhere: 480px (small phones), 768px (tablets), 1024px
(small laptops), 1280px (desktop). These are reasonable starting points, not a spec to memorize or
match to any real device catalog - device sizes vary too much for that to matter.

The actual rule: **resize your own browser window and add a breakpoint wherever your own content starts
looking bad.** If the three-card row gets cramped at 700px, that's your breakpoint - not 768px because a
list said so. Content-driven breakpoints track what you actually built; device-driven ones drift out of
date the moment a new screen size ships.

Check your understanding of the viewport tag and media queries:

```quiz
[
  {
    "q": "Why does a page look tiny and zoomed out on a phone without the viewport meta tag?",
    "choices": [
      "The phone's screen resolution is too low to render CSS",
      "The browser lays out the page at a wide virtual width (like 980px) and shrinks the result to fit the screen",
      "Media queries are disabled by default on mobile browsers",
      "The phone ignores the CSS file entirely without the tag"
    ],
    "answer": 1,
    "explain": "Mobile browsers default to a wide virtual layout viewport for compatibility with old desktop-only sites, then zoom the result out to fit the physical screen."
  },
  {
    "q": "What does `@media (max-width: 600px) { ... }` mean?",
    "choices": [
      "Apply these rules only when the viewport is exactly 600px wide",
      "Apply these rules when the viewport is 600px wide or narrower",
      "Apply these rules when the viewport is 600px wide or wider",
      "Apply these rules only on mobile devices, regardless of width"
    ],
    "answer": 1,
    "explain": "max-width matches at or below the given width. min-width is the opposite: at or above."
  },
  {
    "q": "Where should breakpoint values actually come from?",
    "choices": [
      "A fixed industry standard that never changes",
      "Wherever your own content starts to break, found by resizing the browser",
      "The exact pixel width of the most popular phone model",
      "Whatever number a CSS framework hardcodes"
    ],
    "answer": 1,
    "explain": "Common numbers like 768px are reasonable starting guesses, but the real signal is your own layout breaking at a given width."
  }
]
```


---

# Fluid Layouts

Media queries fix specific breakpoints, but they're patches on top of a layout that's fundamentally
rigid. A fluid layout bends on its own between breakpoints, so you need fewer of them. The tool is
simple: stop hardcoding pixel widths, and let the browser do the flexing.

## The problem with fixed pixels

Here's the card layout from Phase 1, but built the way a lot of beginners first reach for it - fixed
widths instead of `flex: 1`:

```css
.cards {
  display: flex;
  gap: 16px;
}

.card {
  width: 300px;
  padding: 24px;
  border: 1px solid #ddd;
}
```

Three cards at `300px` plus gaps need at least 964px of space. On a 1440px desktop, fine. On a 375px
phone, the cards don't shrink - they overflow, and the whole page grows a horizontal scrollbar. The
browser won't compress a fixed width to fit; content spills past the viewport edge instead.

## The fix: relative units and wrapping

Swap fixed widths for flexible ones:

```css
.cards {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}

.card {
  flex: 1 1 280px;
  padding: 24px;
  border: 1px solid #ddd;
}
```

`flex: 1 1 280px` means: grow to fill space, shrink if needed, but use 280px as the natural starting
size. Combined with `flex-wrap: wrap`, cards that no longer fit on one row drop to the next row instead
of overflowing. At 1440px you get three cards per row; around 900px, two; below 600px, one per row -
all without writing a single media query. Grid does the same job with `repeat(auto-fit, minmax(280px, 1fr))`
on `grid-template-columns`, which wraps columns automatically as space runs out.

This is the core habit: reach for units and layout modes that respond to available space before reaching
for a breakpoint. Media queries still matter for bigger structural changes (Phase 1's stacked-column
example), but fluid layout should handle the small adjustments on its own.

## Relative units over `px`

| Unit | Relative to | Good for |
|------|-------------|----------|
| `%` | Parent element's size | Widths inside a flexible container |
| `rem` | Root (`html`) font size | Font sizes, spacing - scales if the user changes browser font size |
| `em` | Current element's font size | Spacing that should scale with local text size |
| `vw`/`vh` | Viewport width/height | Full-bleed sections, fluid type (see below) |

A card grid built with `padding: 24px` ignores a user who bumped their browser's default font size for
readability. `padding: 1.5rem` (24px at the default 16px root size) scales with that setting. Pixels
aren't banned - border widths and small fixed details are fine in `px` - but layout dimensions and type
should default to relative units.

## `clamp()` for fluid values without breakpoints

`clamp(min, preferred, max)` picks a value that scales smoothly between a floor and a ceiling:

```css
h2 {
  font-size: clamp(1.25rem, 4vw, 2rem);
}
```

Below the width where `4vw` equals `1.25rem`, the font size stays at the `1.25rem` floor. Above the
width where `4vw` hits `2rem`, it caps there. In between, the size scales continuously with viewport
width - no jump at a breakpoint, no separate rule for tablet.

Apply the same idea to spacing:

```css
.cards {
  gap: clamp(8px, 2vw, 24px);
}

.card {
  padding: clamp(16px, 3vw, 32px);
}
```

Gaps and padding shrink on small screens and grow on large ones, continuously, with one line each.

## Before and after: the full card layout

Fixed-width version - breaks below ~964px, overflows on phones:

```css
.cards {
  display: flex;
  gap: 16px;
}

.card {
  width: 300px;
  padding: 24px;
  border: 1px solid #ddd;
}

.card h3 {
  font-size: 24px;
}
```

Fluid version - wraps at any width, spacing and type scale on their own:

```css
.cards {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(8px, 2vw, 24px);
}

.card {
  flex: 1 1 280px;
  padding: clamp(16px, 3vw, 32px);
  border: 1px solid #ddd;
}

.card h3 {
  font-size: clamp(1.1rem, 3vw, 1.5rem);
}
```

Same three cards, same visual intent, but the second version needs zero media queries to survive the
jump from a 320px phone to a 1920px monitor. You'd still add a media query for a structural change -
say, hiding a sidebar entirely below 700px - but the day-to-day sizing is handled by the layout itself.

Check your understanding of fluid layout:

```quiz
[
  {
    "q": "Why does a flex item with `width: 300px` cause a horizontal scrollbar on a narrow phone screen?",
    "choices": [
      "Fixed widths automatically convert to percentages on small screens",
      "The browser refuses to shrink a fixed width below its set value, so content overflows the viewport",
      "Phones ignore the width property entirely",
      "flex-wrap is required for width to work at all"
    ],
    "answer": 1,
    "explain": "A fixed pixel width is a hard floor - the browser won't compress it, so if three of them don't fit, the layout overflows instead of shrinking."
  },
  {
    "q": "What does `clamp(1.25rem, 4vw, 2rem)` do?",
    "choices": [
      "Always renders the font at exactly 4vw",
      "Picks whichever value is smallest",
      "Scales the value with viewport width, but never below 1.25rem or above 2rem",
      "Applies 1.25rem on mobile and 2rem on desktop with no in-between"
    ],
    "answer": 2,
    "explain": "clamp(min, preferred, max) scales continuously between the floor and ceiling, using the preferred value when it falls in range."
  },
  {
    "q": "Which combination lets a row of cards wrap onto a new line automatically as space runs out?",
    "choices": [
      "display: block with fixed widths",
      "flex-wrap: wrap with a flex-basis like flex: 1 1 280px",
      "position: absolute on each card",
      "A media query for every possible screen width"
    ],
    "answer": 1,
    "explain": "flex-wrap: wrap plus a flexible basis lets items reflow onto new rows without any breakpoint-specific CSS."
  }
]
```


---

# Responsive Images and Mobile-First Workflow

A single 2400px-wide hero image looks great on a 4K monitor and wastes bandwidth on a phone that
displays it at 380px. Responsive images fix that by letting the browser pick a file size that matches
the screen. Then there's a second habit worth building alongside it: which direction you write your
media queries, because it changes how tangled your stylesheet gets.

## `srcset` and `sizes`: same image, different resolutions

Add each card in the layout an image, and provide multiple resolutions of the same picture:

```html
<img
  src="card-800.jpg"
  srcset="card-400.jpg 400w, card-800.jpg 800w, card-1200.jpg 1200w"
  sizes="(max-width: 600px) 100vw, 33vw"
  alt="Product photo">
```

- `srcset` lists candidate files with their real pixel width (`400w` means that file is 400px wide).
- `sizes` tells the browser how wide the image will actually render at different viewport widths -
  here, full viewport width below 600px (one card per row), or a third of the viewport above it (three
  cards per row).
- `src` is the fallback for browsers that don't support `srcset`.

The browser combines its own screen width and pixel density with `sizes` to pick the smallest `srcset`
candidate that still looks sharp, and downloads only that one file. Nothing to compute by hand - you
supply the options and the rendered size, the browser picks.

This solves a bandwidth problem, not a content problem: it's still fundamentally the same photo at
different resolutions.

## `<picture>`: swapping the image itself

Sometimes the fix isn't a smaller version of the same photo - it's a different crop or composition
entirely. A wide banner photo showing three people side by side reads fine on desktop, but on a phone,
shrunk down, faces disappear into a blur. That's **art direction**, and it needs `<picture>`:

```html
<picture>
  <source media="(max-width: 600px)" srcset="team-portrait-crop.jpg">
  <source media="(max-width: 1024px)" srcset="team-wide-crop.jpg">
  <img src="team-full.jpg" alt="The team at our office">
</picture>
```

The browser checks each `<source>` top to bottom and uses the first one whose `media` condition
matches, falling back to the `<img>` if none do. Below 600px it loads a tighter crop focused on faces;
between 600-1024px, a medium crop; above that, the full wide shot.

Rule of thumb: **`srcset`/`sizes` for the same image at different sizes (resolution switching);
`<picture>` when the image content itself should change (art direction).** Most product/card images
only need `srcset`. Hero banners and portraits often need `<picture>`.

## Mobile-first: write `min-width` and layer up

Two ways to structure a stylesheet with breakpoints. Desktop-first starts with the wide layout as the
default, then uses `max-width` queries to strip things down for smaller screens:

```css
.cards {
  display: flex;
  gap: 24px;
}

.card {
  width: 30%;
}

@media (max-width: 900px) {
  .card { width: 45%; }
}

@media (max-width: 600px) {
  .cards { flex-direction: column; }
  .card { width: 100%; }
}
```

Mobile-first flips it: the default styles target the smallest screen, and `min-width` queries add
complexity as the screen grows:

```css
.cards {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.card {
  width: 100%;
}

@media (min-width: 600px) {
  .cards { flex-direction: row; flex-wrap: wrap; }
  .card { width: 45%; }
}

@media (min-width: 900px) {
  .card { width: 30%; gap: 24px; }
}
```

Both render the same result at every width. The difference shows up in how you think while writing
them. Desktop-first means every mobile fix is an override fighting the desktop default - you write a
rule, then write another rule to partially undo it, and the two live far apart in the file. Mobile-first
means each larger breakpoint only *adds* capability (more columns, more spacing) on top of a base that
already works everywhere, so there's less to undo and less to hold in your head at once.

Mobile-first also matches how phones actually load pages: a phone never has to parse and immediately
override desktop rules it will never use, since the base styles already target it.

This isn't a hard rule - a desktop-heavy internal dashboard where mobile is an afterthought can
reasonably go desktop-first. But for anything public-facing where phone traffic matters, mobile-first
tends to produce a shorter, less tangled stylesheet by the time the project has five breakpoints instead
of two.

Check your understanding of responsive images and mobile-first CSS:

```quiz
[
  {
    "q": "When should you reach for `<picture>` instead of `srcset`/`sizes`?",
    "choices": [
      "Whenever you have more than one image on the page",
      "When you need the actual image content to change (different crop or composition), not just resolution",
      "srcset and picture always do the same thing",
      "picture is only for background images in CSS"
    ],
    "answer": 1,
    "explain": "srcset/sizes serves the same image at different resolutions. picture swaps in a genuinely different image - art direction - based on conditions."
  },
  {
    "q": "In a mobile-first stylesheet, what do the base (non-media-query) styles target?",
    "choices": [
      "The widest screen the site supports",
      "The narrowest/smallest screen, with min-width queries adding complexity as width grows",
      "Whatever screen size the developer's monitor happens to be",
      "Print styles"
    ],
    "answer": 1,
    "explain": "Mobile-first means the default rules are the small-screen layout; min-width media queries layer on additional styling for larger viewports."
  },
  {
    "q": "Why does mobile-first tend to produce a less tangled stylesheet than desktop-first?",
    "choices": [
      "Because min-width queries execute faster in the browser than max-width queries",
      "Because each breakpoint only adds capability on top of a working base, instead of every mobile rule being an override fighting a desktop default",
      "Because mobile-first stylesheets don't need media queries at all",
      "There's no real difference - it's purely a style preference"
    ],
    "answer": 1,
    "explain": "Desktop-first requires override-on-override to strip a wide layout down; mobile-first only adds rules as space becomes available, which stays easier to follow as breakpoints multiply."
  }
]
```

## Where to go next

Responsive layout handles screen size, but not every visitor navigates by sight or with a mouse. Pair
this guide with [Accessibility From Day One](/guides/accessibility-from-day-one) to make sure the same
layout also works with keyboards, screen readers, and zoomed text.
