Responsive Design & Fluid Layouts
Responsive design used to mean "add breakpoints until it looks right on an iPhone." Modern responsive design means components that adapt to their context â not just the viewport â using fluid scales, container queries, and intrinsic sizing.
Media Queries: The Foundation
Breakpoint Strategy
/* Mobile-first: base styles are mobile, layers add for larger screens */
.card {
padding: 1rem;
grid-template-columns: 1fr;
}
@media (min-width: 48rem) { /* 768px */
.card {
padding: 1.5rem;
grid-template-columns: 1fr 1fr;
}
}
@media (min-width: 64rem) { /* 1024px */
.card {
padding: 2rem;
grid-template-columns: 1fr 1fr 1fr;
}
}Mobile-first vs desktop-first:
| Approach | Base styles | Media queries | Advantage |
|---|---|---|---|
| Mobile-first | Small screen | min-width to add complexity | Progressive enhancement, smaller mobile CSS |
| Desktop-first | Large screen | max-width to remove features | Legacy migration, complex desktop layouts |
Mobile-first is almost always the right default. Mobile styles are simpler, so they make a better baseline.
Preference Queries
/* Dark mode */
@media (prefers-color-scheme: dark) {
:root {
--color-bg: oklch(0.15 0 0);
--color-text: oklch(0.9 0 0);
}
}
/* Reduced motion â critical for accessibility */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* High contrast */
@media (prefers-contrast: more) {
:root {
--color-border: black;
--color-text: black;
}
}
/* Reduced data â skip non-essential resources */
@media (prefers-reduced-data: reduce) {
.hero-video { display: none; }
.hero-image { content: url('/hero-placeholder.svg'); }
}Container Queries: Component-Level Responsiveness
Media queries respond to the viewport. Container queries respond to the parent container. This is the shift from page-level to component-level responsive design.
/* Define a containment context */
.card-wrapper {
container-type: inline-size;
container-name: card;
}
/* Query the container, not the viewport */
@container card (min-width: 400px) {
.card {
grid-template-columns: 200px 1fr;
}
}
@container card (min-width: 600px) {
.card {
grid-template-columns: 250px 1fr 1fr;
}
}Container Types
| Type | What's contained | Use case |
|---|---|---|
inline-size | Width only (most common) | Cards, sidebar components |
size | Width and height | Aspect-ratio dependent layouts |
normal | No containment (default) | Not queryable |
Container Query Units
.card-title {
font-size: clamp(1rem, 3cqi, 1.5rem);
/* cqi = 1% of container's inline size */
}| Unit | Relative to |
|---|---|
cqw | Container width |
cqh | Container height |
cqi | Container inline size |
cqb | Container block size |
cqmin | Smaller of cqi/cqb |
cqmax | Larger of cqi/cqb |
Why Container Queries Change Everything
/* Without container queries: sidebar card vs main card */
.card { grid-template-columns: 1fr; }
@media (min-width: 768px) {
.main .card { grid-template-columns: 200px 1fr; }
}
@media (min-width: 1024px) {
.sidebar .card { grid-template-columns: 1fr; }
}The component must know its context (.main vs .sidebar). With container queries, the card adapts based on its actual available space â no context knowledge needed.
Fluid Typography with clamp()
clamp(min, preferred, max) creates smooth scaling between bounds:
:root {
--text-sm: clamp(0.875rem, 0.8rem + 0.25vw, 1rem);
--text-base: clamp(1rem, 0.9rem + 0.35vw, 1.125rem);
--text-lg: clamp(1.25rem, 1rem + 0.75vw, 1.5rem);
--text-xl: clamp(1.5rem, 1rem + 1.5vw, 2.25rem);
--text-2xl: clamp(2rem, 1.25rem + 2.5vw, 3.5rem);
--space-fluid-sm: clamp(0.5rem, 0.3rem + 0.5vw, 0.75rem);
--space-fluid-md: clamp(1rem, 0.75rem + 1vw, 1.5rem);
--space-fluid-lg: clamp(1.5rem, 1rem + 2vw, 3rem);
}
h1 { font-size: var(--text-2xl); }
p { font-size: var(--text-base); }The formula clamp(min, preferred, max) with preferred = base + rate à vw creates a linear interpolation between two viewport sizes. The min and max cap the extremes.
Font size
â
â min âââââââââ preferred slope âââââââââ max
â âą âē
âââââââââą âēââââââ
â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ viewport width
320px 1200pxResponsive Images
The <picture> Element
<picture>
<!-- Art direction: different crop for different sizes -->
<source media="(min-width: 1024px)" srcset="/hero-wide.webp" type="image/webp">
<source media="(min-width: 640px)" srcset="/hero-medium.webp" type="image/webp">
<img src="/hero-small.jpg" alt="Hero" width="640" height="360">
</picture>srcset and sizes for Resolution Switching
<img
srcset="
/photo-400.webp 400w,
/photo-800.webp 800w,
/photo-1200.webp 1200w,
/photo-1600.webp 1600w
"
sizes="
(min-width: 1024px) 50vw,
(min-width: 640px) 75vw,
100vw
"
src="/photo-800.webp"
alt="Responsive photo"
width="800"
height="600"
loading="lazy"
decoding="async"
>sizes tells the browser how wide the image will be displayed so it can choose the right srcset candidate before downloading. Without sizes, the browser assumes 100vw.
Viewport Units
| Unit | Reference | Behavior |
|---|---|---|
vh / vw | Viewport height/width | Classic â but vh fails on mobile |
svh / svw | Small viewport | Excludes dynamic toolbars (smallest viewport) |
lvh / lvw | Large viewport | Includes dynamic toolbars (largest viewport) |
dvh / dvw | Dynamic viewport | Updates as toolbars show/hide |
vi / vb | Viewport inline/block | Logical, respects writing mode |
/* The mobile 100vh problem: content hides behind browser chrome */
.hero {
min-height: 100vh; /* Broken on mobile â extends behind toolbar */
}
/* The fix: dynamic viewport height */
.hero {
min-height: 100dvh; /* Adjusts as mobile toolbar shows/hides */
}
/* Safe fallback pattern */
.hero {
min-height: 100vh;
min-height: 100dvh;
}Logical Properties
Logical properties replace physical directions with flow-relative ones, enabling automatic RTL support:
/* Physical (breaks in RTL) */
.card {
margin-left: 1rem;
padding-right: 2rem;
border-bottom: 1px solid;
width: 300px;
}
/* Logical (works in any writing mode) */
.card {
margin-inline-start: 1rem;
padding-inline-end: 2rem;
border-block-end: 1px solid;
inline-size: 300px;
}| Physical | Logical | Maps to (LTR) |
|---|---|---|
width | inline-size | Horizontal |
height | block-size | Vertical |
margin-left | margin-inline-start | Left |
margin-right | margin-inline-end | Right |
margin-top | margin-block-start | Top |
padding-left | padding-inline-start | Left |
border-bottom | border-block-end | Bottom |
top | inset-block-start | Top |
Responsive Component Patterns
Fluid Grid with Intrinsic Breakpoints
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
gap: var(--space-fluid-md);
}This grid adapts without a single media query: cards are at least 300px wide (or full width if container is < 300px), and the number of columns adjusts automatically.
Responsive Stack
.stack {
display: flex;
flex-wrap: wrap;
gap: var(--space-fluid-md);
}
.stack > * {
flex: 1 1 min(100%, 20rem);
}Items sit side-by-side when there's room, and stack vertically when the container narrows â driven entirely by content, not breakpoints.
Container-Responsive Card
.card-container { container-type: inline-size; }
.card {
display: grid;
gap: 1rem;
padding: var(--space-fluid-md);
}
@container (min-width: 30rem) {
.card {
grid-template-columns: 12rem 1fr;
grid-template-rows: auto 1fr auto;
}
.card__image {
grid-row: 1 / -1;
}
}Interview Signal
Senior candidates demonstrate:
- Mobile-first reasoning â Explaining why progressive enhancement produces better CSS than desktop-first degradation
- Container query mastery â Using container queries for component-level responsive design, understanding containment types
- Fluid design â Building layouts with
clamp(), intrinsic sizing, and no breakpoints where possible - Image strategy â
srcset+sizesfor resolution switching,<picture>for art direction, explaining the performance impact - Viewport awareness â Understanding the mobile
100vhproblem and when to usedvhvssvhvslvh