GAP·MAP

Responsive design

[ junior depth ]

Start with a layout that can shrink

Plain HTML reflows as the viewport narrows. Responsive CSS should preserve that ability. A fixed page width removes it and can produce horizontal scrolling on narrow screens.

/* Brittle */
.page {
  width: 1200px;
}

/* Fluid, with a readable upper bound */
.page {
  width: calc(100% - 2rem);
  max-width: 75rem;
  margin-inline: auto;
}

The percentage follows the containing block. The rem values follow the root element's font size. The page can shrink, but it stops growing at 75rem.

Wrong "Responsive design means adding breakpoints for phone, tablet, and desktop."

Right Add a breakpoint where the content needs a different arrangement. MDN's guidance is to change the design when content starts to break, such as when a sidebar becomes too narrow or lines become too long. A device catalog cannot cover the range of viewport sizes.

Mobile-first CSS

Mobile-first CSS begins with the smallest view and adds layout when more width is available. A document in normal flow often gives a readable single column without much layout code.

.shell {
  display: block;
}

@media (width >= 48rem) {
  .shell {
    display: grid;
    grid-template-columns: 2fr 1fr;
    gap: 2rem;
  }
}

The first rule applies at every width. The media query enhances the page when the viewport is at least 48rem wide. Media queries can test viewport dimensions and other user-agent or device features, including orientation and user preferences.

Mobile-first describes the direction in which the CSS adds layout. It does not mean that every narrow screen is a phone.

Fluid values with clamp()

clamp(minimum, preferred, maximum) selects the preferred expression while it is between the two bounds. Below the range it uses the minimum; above the range it uses the maximum.

h1 {
  font-size: clamp(2rem, 1.5rem + 2vw, 4rem);
}

.section {
  padding-block: clamp(1rem, 0.75rem + 1vw, 2rem);
}

The vw term changes with viewport width. The rem bounds prevent the value from shrinking or growing without limit. This creates a continuous range, so there is no jump at a breakpoint.

For font sizes, MDN recommends a relative maximum that is at least twice the minimum to help text scale to 200 percent under zoom:

h1 {
  font-size: clamp(1rem, 2.5vw, 2rem);
}

Wrong "clamp() chooses one of three fixed sizes."

Right The middle argument is the preferred value. It can vary continuously, and the outer arguments bound its result. A layout that must switch from one column to two still needs a layout rule, whether that rule comes from Grid behavior, a media query, or a container query.

Pick a unit by its reference

There is no universally responsive unit. The reference decides what moves:

  • % uses the basis defined by the property. For example, percentage width uses the containing block's width, while percentage font-size uses the parent's font size.
  • rem follows the root font size.
  • em follows the element's font size, or the parent's font size when used on font-size.
  • vw and vh follow viewport width and height.
  • cqi and other container query units follow an eligible query container.

The middle and senior notes apply these references to reusable components, available-space-driven layouts, and intrinsic sizing.

[ middle depth ]

The viewport and the component answer different questions

A media query can test the viewport and other features of the user agent or device. That is useful when the page shell changes or when CSS responds to a user preference.

.page-shell {
  display: grid;
}

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

.carousel {
  scroll-behavior: smooth;
}

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

A reusable component may receive different widths at the same viewport size. A card in a main column and the same card in a sidebar cannot distinguish those allocations with @media. A size container query tests an ancestor instead.

.card-slot {
  container-type: inline-size;
}

.card {
  display: grid;
  grid-template-columns: 1fr;
}

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

container-type: inline-size establishes a query container for its inline dimension. The unnamed @container rule uses the nearest eligible ancestor. The query styles descendants of that container, so the containment context belongs on a wrapper or slot rather than on the card that is trying to query its own size.

Wrong "Container queries are newer media queries, so they should replace them."

Right Their inputs differ. Use a media query for the page's environment and a size container query for space allocated to a component.

Name a query container when nesting matters

An unnamed query can start reading a nearer eligible ancestor after a component is moved or wrapped. A container name filters eligible ancestors by that name and makes the dependency explicit.

.product-region {
  container: product-list / inline-size;
}

@container product-list (width >= 40rem) {
  .product-card {
    grid-template-columns: 12rem 1fr;
  }
}

The container shorthand above assigns the name and type. This is useful when several query containers can appear in the ancestor chain.

Let available space create the column count

Some layouts do not need a breakpoint. Grid can create as many columns as fit into the available container:

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: 1rem;
}

In this pattern, auto-fit creates the repetitions that fit and collapses empty repeated tracks. Each populated track has a 16rem minimum and can take a share of remaining space up to 1fr. Column count therefore follows available width without a rule that says when there must be two, three, or four columns.

This pattern responds to available space, but its 16rem minimum is chosen by the author rather than derived from the cards' content. CSS represents content-derived sizes with keywords such as min-content and max-content. For text, min-content wraps at every available soft wrap and is often governed by the longest unbreakable segment. max-content lays the text out without soft wrapping and can overflow.

.label {
  inline-size: max-content;
}

.narrow-label {
  inline-size: min-content;
}

Wrong "max-content means fill all available space."

Right It is the content's maximum intrinsic size, as if the content had unlimited inline space. It may be wider than its container.

Fluid scales are bounded interpolation

Viewport-based values can move between intentional bounds:

:root {
  --space-section: clamp(2rem, 1rem + 3vw, 5rem);
  --title-size: clamp(2rem, 1.4rem + 2.5vw, 4.5rem);
}

This works for values that should change continuously. It cannot express a discrete change such as switching display, changing a grid template, or replacing a page navigation pattern. Use a query for that kind of switch. Use intrinsic sizing or an available-space-driven layout when sizing and wrapping can express the design.

[ senior depth ]

Responsive behavior belongs at the right boundary

Media queries inspect the user agent or device environment. Size container queries inspect the dimensions of a query container. Grid and Flexbox can respond to available space without a conditional rule, while intrinsic sizing derives sizes from content. A resilient design can use these mechanisms together without making them duplicate each other.

/* Page concern: the viewport has room for persistent navigation. */
.app-shell {
  display: grid;
}

@media (width >= 70rem) {
  .app-shell {
    grid-template-columns: 18rem 1fr;
  }
}

/* Component concern: this allocated slot can hold a horizontal card. */
.result-slot {
  container: result / inline-size;
}

.result-card {
  display: grid;
  grid-template-columns: 1fr;
}

@container result (width >= 34rem) {
  .result-card {
    grid-template-columns: 11rem 1fr;
  }
}

The breakpoint values are design inputs, not device classifications. Test the content across widths and introduce a conditional change where its current arrangement stops working.

Wrong "A shared breakpoint scale should control every responsive component."

Right Shared tokens can document recurring layout thresholds, but a component whose behavior depends on its allocation should query that allocation. Two instances at one viewport width can then render differently.

Containment prevents circular size decisions

A size query needs an eligible container. container-type: inline-size applies layout, style, and inline-size containment to the element. The browser can determine the container's inline size without letting queried descendant styles create a circular dependency on that same size.

The target of @container is a descendant of the query container. Put the containment context on the component's slot:

<div class="profile-slot">
  <article class="profile-card">...</article>
</div>
.profile-slot {
  container-type: inline-size;
}

@container (width >= 30rem) {
  .profile-card {
    display: grid;
    grid-template-columns: auto 1fr;
  }
}

Putting container-type on .profile-card does not make this rule a self-query. The query selects an eligible ancestor for the elements being styled.

Container-relative fluid values

Container query length units make a value relative to a query container: cqw is one percent of its width, while cqi is one percent of its inline size. The logical cqi follows writing mode and is often the better match for inline layout.

.hero-slot {
  container: hero / inline-size;
}

.hero-title {
  font-size: clamp(1.75rem, 1.25rem + 3cqi, 3.5rem);
}

The title now responds to its component region instead of the viewport. If no eligible query container exists, container query length units fall back to the small viewport unit for the relevant axis. Establishing the intended container makes the dependency visible and avoids relying on that fallback.

For viewport height, CSS defines small, large, and dynamic viewport units in addition to default viewport units. Their references differ, so choose among svh, lvh, and dvh according to whether a layout needs the small, large, or changing viewport size.

.stable-screen {
  min-block-size: 100svh;
}

.live-screen {
  min-block-size: 100dvh;
}

Wrong "Relative units are interchangeable because they all scale."

Right Each unit names a dependency. rem follows the root font, % follows the property-specific percentage basis, viewport units follow a viewport, and container units follow a query container.

Layout algorithms reduce conditional CSS

Before adding a query, check whether the layout can express its constraint directly:

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
}

.toolbar {
  display: flex;
  flex-wrap: wrap;
}

Grid can fit repeated tracks to available width. In this example, 15rem is an author-defined minimum, not an intrinsic content size. Flexbox can wrap items when a line lacks space. Queries remain appropriate when the design must make a discrete semantic or structural choice that the layout algorithm cannot express through sizing and wrapping alone.

clamp() has the same boundary. It constrains a preferred numeric expression between minimum and maximum values. It does not create a conditional branch:

.section-slot {
  container-type: inline-size;
}

.section {
  padding-block: clamp(1.5rem, 1rem + 2cqi, 4rem);
}

.section-layout {
  display: grid;
  grid-template-columns: 1fr;
}

@container (width >= 50rem) {
  .section-layout {
    grid-template-columns: 2fr 1fr;
  }
}

The first rule handles continuous spacing. The second handles a discrete track change.

dig deeper

Primary sources behind these notes - the specs and official docs worth reading in full.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

builds on

unlocks

more CSS

was this useful?