← Back to Index

Lesson 22

Responsive Design Patterns - Composing Layout Systems

You've learned the individual tools: flexbox, grid, container queries, clamp(), and logical properties. This lesson is about composition - combining them into real-world responsive patterns that adapt without breakpoints, or with minimal breakpoints.

The mental model: intrinsic vs extrinsic design

Extrinsic design = you tell the layout what size to be (width: 800px, @media (min-width: 768px)). Brittle - tied to specific viewport widths.

Intrinsic design = the layout figures out its own size based on content and available space (minmax(), auto-fill, clamp(), container queries). Fluid - adapts to any context without knowing the viewport.

Modern responsive design is about writing as much as possible intrinsically, and only reaching for media queries when you genuinely need to respond to the viewport (e.g. navigation patterns, print styles).

Pattern 1: Fluid typography with clamp()

Resize the browser - the heading and paragraph scale smoothly between min and max

This heading uses clamp()

Body text also scales fluidly. No breakpoints needed - it smoothly interpolates between the min and max font sizes based on viewport width.

/* Fluid type — no breakpoints */
h3 {
  /* min: 1rem, preferred: 0.5rem + 2vw, max: 2rem */
  font-size: clamp(1rem, 0.5rem + 2vw, 2rem);
}
p {
  font-size: clamp(0.85rem, 0.75rem + 0.5vw, 1.1rem);
}

/* The formula: clamp(MIN, PREFERRED, MAX)
   PREFERRED uses a mix of rem (base) + vw (viewport-responsive)
   - rem ensures it doesn't collapse below readable on zoom
   - vw makes it grow with the viewport
   - clamp() caps both ends */

The clamp() formula pattern: clamp(min, base + growth, max) where:

min = smallest acceptable size (usually in rem)

base + growth = a rem value + a vw value that scales with viewport

max = largest acceptable size (usually in rem)

This works for font-size, padding, gap - anything that should scale fluidly.

Pattern 2: Auto-filling grid (no breakpoints)

The most useful intrinsic pattern: a grid that automatically adjusts column count based on available space. No media queries, no JS, works inside any container.

Drag the right edge to resize - columns appear and disappear automatically

Item 1
Item 2
Item 3
Item 4
Item 5
Item 6

↔ drag to resize

/* The magic one-liner */
.auto-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 0.75rem;
}

/* How it works:
   - auto-fill: create as many columns as will fit
   - minmax(140px, 1fr): each column is at least 140px, stretches to fill
   - Result: 1 column on small, 2 on medium, 3+ on wide — no breakpoints */

auto-fill vs auto-fit: Both create columns to fill space. The difference shows when there are fewer items than columns:

auto-fill keeps empty tracks (space is preserved for potential items)

auto-fit collapses empty tracks (items stretch to fill all available space)

For most card grids, auto-fill is what you want. Use auto-fit when you have few items and want them to stretch.

Pattern 3: Container-query-responsive card

Drag to resize - the card switches from stacked to horizontal based on container width

Adaptive Card

I stack vertically when narrow, go horizontal when my container is 350px+. No media queries - container queries.

↔ drag to resize

/* Establish the containment context */
.card-wrapper {
  container-type: inline-size;
}

/* Default: stacked (narrow) */
.card {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.75rem;
}

/* When the CONTAINER (not viewport) is wide enough: horizontal */
@container (min-width: 350px) {
  .card {
    grid-template-columns: 120px 1fr;
  }
}

When to use container queries vs media queries:

Container queries for components that might be placed in different-width contexts (cards, widgets, sidebars). The component adapts to where it lives.

Media queries for page-level layout decisions that genuinely depend on the viewport (nav collapse, overall page structure).

Rule of thumb: if it's a reusable component, prefer container queries. If it's the page shell, use media queries.

Ad

Pattern 4: The pancake stack (header/main/footer)

Header and footer take their natural height; main expands to fill remaining space

Header - auto height (takes what it needs)
Main - 1fr (fills all remaining space)
/* The pancake stack */
.page {
  display: grid;
  grid-template-rows: auto 1fr auto;
  min-height: 100vh;  /* or 100dvh for mobile */
}

/* That's it. Header and footer take their natural height.
   Main gets everything else. No flexbox hacks needed. */

Pattern 5: The holy grail (named grid areas)

Named grid areas - the template reads like a visual map of the layout

Header
Nav
Main content area
Aside
/* The layout reads like a visual map */
.layout {
  display: grid;
  grid-template:
    "header header header" auto
    "nav    main   aside"  1fr
    "footer footer footer" auto
    / minmax(0, 12rem) 1fr minmax(0, 12rem);
  gap: 0.5rem;
  min-height: 100vh;
}

.header { grid-area: header; }
.nav    { grid-area: nav; }
.main   { grid-area: main; }
.aside  { grid-area: aside; }
.footer { grid-area: footer; }

/* Collapse to single column on small viewports */
@media (max-width: 768px) {
  .layout {
    grid-template:
      "header" auto
      "nav"    auto
      "main"   1fr
      "aside"  auto
      "footer" auto
      / 1fr;
  }
}

Why named areas over line numbers: grid-template with named areas is a visual ASCII map of your layout. You can read the structure without counting lines. And when you rearrange for mobile, you rewrite the map - the HTML doesn't change.

Pattern 6: Sidebar that collapses (intrinsic)

Drag to resize - sidebar drops below main when space is tight

Sidebar
flex: 1 1 200px - needs at least 200px, otherwise wraps.
Main content
flex: 3 1 400px - takes 3x the space of the sidebar when both fit.

↔ drag to resize

/* Intrinsic sidebar — no breakpoints */
.with-sidebar {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
}
.sidebar {
  flex: 1 1 200px;  /* grow, shrink, but need at least 200px */
}
.content {
  flex: 3 1 400px;  /* 3x the growth factor, need at least 400px */
}

/* How it works:
   - When container > 600px: both fit side-by-side (200 + 400 basis)
   - When container < 600px: content can't get 400px → wraps to next line
   - Both go full-width when wrapped (flex-grow fills the row)
   - No media query needed — the flex-basis thresholds do the work */

Pattern 7: Fluid spacing with clamp()

The gap and padding scale smoothly between min and max values

Card A
Card B
/* Fluid spacing — gap and padding scale with viewport */
.fluid-section {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(0.5rem, 1rem + 1vw, 2rem);
  padding: clamp(0.75rem, 0.5rem + 2vw, 2.5rem);
}

/* Works for margins, block spacing, anything dimensional */
.section + .section {
  margin-block-start: clamp(2rem, 1rem + 3vw, 5rem);
}

Choosing the right tool: a decision framework

/* WHAT ARE YOU MAKING RESPONSIVE?

   The VIEWPORT (page-level layout):
   → media queries + grid-template rearrangement

   A COMPONENT (reusable, placed in varying contexts):
   → container queries

   SPACING / TYPOGRAPHY (fluid scaling):
   → clamp() with rem + vw

   A COLLECTION (unknown number of items in a grid):
   → repeat(auto-fill, minmax(MIN, 1fr))

   A SIDEBAR + MAIN (simple collapse):
   → flex-wrap with flex-basis thresholds

   EVERYTHING:
   → logical properties (inline/block) instead of left/right
*/

The modern responsive hierarchy:

1. Intrinsic sizing first - let content and minmax/clamp handle it

2. Container queries second - for components that need context-awareness

3. Media queries last - only for true viewport-level decisions (nav patterns, print)

If you find yourself writing more than 2-3 media queries for a page, ask whether container queries or intrinsic sizing could replace them.

Retrieval check

Question 1

What does repeat(auto-fill, minmax(200px, 1fr)) do?

Creates exactly 3 columns of 200px each
Creates as many 200px+ columns as fit, each stretching to fill space equally
Creates one column that's between 200px and the full width
Creates columns only when a media query matches

Question 2

When should you use container queries instead of media queries?

Always - media queries are deprecated
For reusable components that might live in different-width contexts
Only when flexbox isn't available
Only for mobile-first designs

Question 3

In clamp(1rem, 0.5rem + 2vw, 2rem), what does the middle value do?

Sets the default before any viewport calculation
Defines the preferred size that scales with viewport, clamped between the min and max
Overrides the min when the viewport is exactly 768px
Provides a fallback for browsers that don't support clamp

Question 4

What's the difference between auto-fill and auto-fit?

auto-fit is for flexbox, auto-fill is for grid
auto-fill creates flexible tracks, auto-fit creates fixed ones
auto-fill keeps empty tracks; auto-fit collapses them so items stretch to fill
They're identical - auto-fit is just newer syntax

Question 5

The flex-wrap sidebar pattern uses flex-basis to control the collapse point. Why does it work without a breakpoint?

Flexbox ignores breakpoints by default
When items can't satisfy their flex-basis in one row, they wrap - the basis IS the implicit breakpoint
It uses container queries internally
The browser calculates the breakpoint from the gap value