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
↔ 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.
Pattern 4: The pancake stack (header/main/footer)
Header and footer take their natural height; main expands to fill 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
/* 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
flex: 1 1 200px - needs at least 200px, otherwise wraps.
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
/* 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?
Question 2
When should you use container queries instead of media queries?
Question 3
In clamp(1rem, 0.5rem + 2vw, 2rem), what does the middle value do?
Question 4
What's the difference between auto-fill and auto-fit?
Question 5
The flex-wrap sidebar pattern uses flex-basis to control the collapse point. Why does it work without a breakpoint?