Last week you built layouts with Flexbox and Grid. They looked great: on your screen, at your screen size. This week you'll make them look great on every screen.
Responsive design is the practice of writing CSS that adapts to different viewport sizes: phones, tablets, laptops, wide monitors, and everything in between. It's not a separate skill layered on top of CSS — it's a way of thinking about CSS from the start. By the end of this lesson, writing responsive code will feel like the natural way to write CSS, not an afterthought.
A note before diving in: the auto-fill/minmax and flex-wrap patterns from last week are already responsive. You've been writing responsive CSS without calling it that. This lesson names the tools, fills in the gaps, and builds the full picture.
A responsive layout reflows and adjusts based on the space it has. Content stays readable, interactive elements stay usable, and nothing overflows or breaks — regardless of screen width.
This comes from three foundational ideas:
Fluid layouts — sizes expressed as proportions rather than fixed pixels, so elements stretch and contract with the viewport.
Flexible images — images that scale down to fit their container instead of overflowing it.
Media queries — CSS rules that activate only when specific conditions are met, like the screen being narrower than 768px.
You'll use all three today!
Before any CSS can do its job on a mobile device, the browser needs to know the page is designed to be responsive. Without this tag, mobile browsers will zoom out and render your page as if it were a desktop site:
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
width=device-width tells the browser to use the actual device width as the viewport width. initial-scale=1 prevents automatic zooming. This tag belongs in every HTML file you create from now on. It's the single line that enables everything else in this lesson.
Before writing responsive layouts, you need the right vocabulary. CSS has two categories of units: absolute (fixed, always the same size) and relative (sized in relation to something else). Responsive design leans heavily on relative units.
https://www.youtube.com/watch?v=8HkGwdUDOcc
px is absolute. 16px is always 16 pixels. This is useful for things that should never change: borders, box shadows, minimum widths. It's less useful for font sizes, spacing, and layout widths — those benefit from being flexible.
rem stands for "root em." It's relative to the font size of the <html> element, which browsers default to 16px.
/* html font-size: 16px (browser default) */
h1 { font-size: 2rem; } /* 32px */
p { font-size: 1rem; } /* 16px */
.small { font-size: 0.875rem; } /* 14px */
The reason rem is preferred over px for typography: if a user has changed their browser's default font size (common for accessibility reasons), rem values scale with it. px values don't.
/* ❌ Ignores user font preferences */
body { font-size: 16px; }
/* ✅ Respects user font preferences */
body { font-size: 1rem; }
rem is also reliable for spacing. A common pattern is to use a base-8 or base-4 spacing scale:
.card {
padding: 1.5rem; /* 24px */
margin-bottom: 2rem; /* 32px */
border-radius: 0.5rem; /* 8px */
}
em is relative to the font size of the current element, not the root. This makes it useful for components where spacing should scale proportionally with the text:
.button {
font-size: 1rem;
padding: 0.75em 1.5em; /* padding scales if font-size changes */
}
.button--large {
font-size: 1.25rem;
/* padding is now 0.75 × 1.25rem = ~15px top/bottom
and 1.5 × 1.25rem = ~24px left/right — scales automatically */
}
The gotcha with em is that it compounds. If a parent has font-size: 1.2em and a child also has font-size: 1.2em, the child's actual size is 1.2 × 1.2 = 1.44rem. For layout spacing where compounding isn't wanted, rem is safer.
<aside> 💡
Info
A practical rule of thumb: use rem for font sizes and global spacing (margins, padding on containers), and em for component-internal spacing that should scale with the component's own font size (button padding, icon sizes).
</aside>
% is always relative to the parent element's corresponding property. For widths, it's relative to the parent's width:
.container {
width: 90%; /* 90% of the parent's width */
max-width: 1200px; /* but never wider than 1200px */
margin: 0 auto; /* horizontally centered */
}
.sidebar {
width: 25%; /* one quarter of the parent */
}
The width: 90%; max-width: 1200px; margin: 0 auto pattern is one of the most common patterns in web development. It creates a centered, fluid container that fills most of the screen on small viewports but caps at a comfortable reading width on large ones.
vw and vh are relative to the viewport width and height respectively. 1vw = 1% of the viewport width, 100vh = the full viewport height.
.hero {
height: 100vh; /* fill the screen height exactly */
width: 100vw; /* fill the screen width exactly */
}
.section {
min-height: 50vh; /* at least half the screen tall */
}
vh is ideal for full-screen sections and hero banners. Be careful with 100vh on mobile browsers — the address bar takes up space and can cause the content to be slightly taller than the visible area. The newer dvh (dynamic viewport height) unit solves this, but browser support is still catching up.
/* Modern approach for full-screen mobile sections */
.hero {
min-height: 100vh;
min-height: 100dvh; /* overrides above in browsers that support it */
}
vw is useful for large typographic elements that should scale with the viewport:
.display-heading {
font-size: clamp(2rem, 5vw, 4rem);
/* never smaller than 2rem, never larger than 4rem,
scales fluidly between based on viewport width */
}
That clamp() function is worth remembering — it defines a minimum, a preferred value, and a maximum. It's the most elegant way to write fluid typography.
<aside> ⌨️
Hands On
Create a simple HTML file with a <div class="container"> wrapping a few paragraphs of text. Give it width: 90%; max-width: 800px; margin: 0 auto. Then open DevTools, toggle the device toolbar (Ctrl/Cmd + Shift + M), and drag the viewport width slider. Watch how the container responds. Then try swapping width: 90% for width: 800px and see the difference when the viewport goes narrow.
</aside>
| Unit | Relative to | Best used for |
|---|---|---|
px |
Nothing (absolute) | Borders, shadows, min-widths |
rem |
Root <html> font size |
Font sizes, spacing, layout |
em |
Current element's font size | Component-internal spacing |
% |
Parent element | Fluid widths, heights |
vh |
Viewport height | Full-screen sections |
vw |
Viewport width | Fluid typography, full-width elements |
A media query is a block of CSS that only applies when a condition is true, usually a condition about the viewport size. Everything inside the block is ignored unless the condition matches.
/* This always applies */
.card {
padding: 1rem;
}
/* This only applies when the viewport is 768px wide or more */
@media (min-width: 768px) {
.card {
padding: 2rem;
}
}
The @media rule wraps normal CSS. When the condition is met, those styles are applied (or overridden). When it isn't, they're skipped entirely.
Breakpoints are the viewport widths where your layout changes. The specific numbers are less important than having a consistent set:
/* A common breakpoint system */
/* Small phones: < 480px — no query needed if mobile-first */
/* Large phones / small tablets */
@media (min-width: 480px) { }
/* Tablets */
@media (min-width: 768px) { }
/* Laptops / small desktops */
@media (min-width: 1024px) { }
/* Wide desktops */
@media (min-width: 1280px) { }
You don't need all of these in every project. Most real projects use 2–3 breakpoints. Let your content decide where breakpoints belong. When a layout starts to look awkward, that's where you add a breakpoint.
<aside> 💡
Info
Chasing device-specific breakpoints (375px for iPhone SE, 390px for iPhone 14...) is a losing game. Devices multiply constantly. Design for content, not for specific devices. A well-chosen small/medium/large breakpoint system will hold up longer than a list of exact pixel values tied to today's popular phones.
</aside>
Media queries can test more than just width:
/* Min-width: apply from this width upward */
@media (min-width: 768px) { }
/* Max-width: apply up to this width (use sparingly in mobile-first) */
@media (max-width: 767px) { }
/* Range: apply between two widths */
@media (min-width: 480px) and (max-width: 767px) { }
/* Orientation */
@media (orientation: landscape) { }
/* User prefers reduced motion — important for accessibility */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms;
transition-duration: 0.01ms;
}
}
/* User prefers dark color scheme */
@media (prefers-color-scheme: dark) {
body {
background: #1a1a1a;
color: #f0f0f0;
}
}
prefers-reduced-motion is worth memorizing early. Animations that look great on a screen can cause real discomfort for users with vestibular disorders (remember the accessibility chapter!). Wrapping your animations with this query is a small effort with meaningful impact.
<aside> ⌨️
Hands On
Add these styles to your file from the previous exercise:
Use DevTools' device toolbar to drag the viewport width across 600px and 900px. You'll see the layout respond at exactly those widths.
</aside>
Mobile-first means writing your base CSS for the smallest screen, then layering larger-screen styles on top using min-width media queries. It's a philosophy, not just a technique.
/* ✅ Mobile-first: base styles for small screens */
.nav {
display: flex;
flex-direction: column; /* stacked on mobile */
gap: 0.5rem;
}
/* Expand for larger screens */
@media (min-width: 768px) {
.nav {
flex-direction: row; /* horizontal on tablet and up */
gap: 2rem;
}
}
/* ❌ Desktop-first: base styles for large screens */
.nav {
display: flex;
flex-direction: row;
gap: 2rem;
}
/* Override for smaller screens */
@media (max-width: 767px) {
.nav {
flex-direction: column;
gap: 0.5rem;
}
}
Both achieve the same result. Mobile-first is preferred because:
Mobile constraints force clarity. If the layout works with minimal space, it almost always works with more space. The reverse isn't true — a desktop layout squeezed into a phone often breaks in unpredictable ways.
It also matches how CSS specificity works. Later rules override earlier ones, so overriding toward larger screens (adding complexity) is cleaner than overriding toward smaller screens (stripping complexity away).
Here's a blog layout that goes from single-column on mobile to a sidebar layout on desktop:
/* Base: single column, mobile */
.blog-layout {
display: grid;
gap: 2rem;
padding: 1rem;
}
.blog-header {
font-size: 1.5rem;
}
.post-card {
padding: 1rem;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
}
/* Tablet: slightly more breathing room */
@media (min-width: 600px) {
.blog-layout {
padding: 2rem;
}
.blog-header {
font-size: 2rem;
}
.post-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
}
/* Desktop: sidebar appears */
@media (min-width: 1024px) {
.blog-layout {
grid-template-columns: 1fr 280px; /* main content + sidebar */
grid-template-areas:
"header header"
"posts sidebar";
max-width: 1200px;
margin: 0 auto;
}
.blog-header { grid-area: header; }
.post-list { grid-area: posts; }
.blog-sidebar { grid-area: sidebar; }
.post-list {
grid-template-columns: repeat(2, 1fr);
}
}