Responsive Design
📱 Chapter 11 — Intro to Responsive Design
A website that looks great at 1440px but is unusable on a phone is only half a website. Responsive design is the practice of building layouts that adapt fluidly to any screen size — from a 320px phone to a 4K monitor — using the same HTML. This chapter covers the core tools: the viewport meta tag, relative units, max-width, and media queries. These are the building blocks you need before applying the Flexbox and Grid techniques from Chapters 8 and 9 to real responsive layouts.
1 — The Problem Responsive Design Solves
A fixed-pixel layout built for a 1200px desktop will simply overflow on a phone. The user has to pinch-zoom and scroll horizontally — a frustrating experience. Before CSS responsive tools existed, developers built separate mobile sites (often at m.example.com). Today, a single set of CSS rules adapts to every screen using fluid units and conditional rules.
section
Notice that the layout shifts at different widths — stacked cards on mobile, two columns on tablet, three on desktop. The nav collapses to a hamburger menu on mobile. All of this is controlled purely by CSS.
2 — The Viewport Meta Tag
Before you write a single media query, you must add one line to your HTML <head>. Without it, mobile browsers will assume your page is a desktop page and zoom it out to fit — making everything tiny and unreadable regardless of your CSS.
<meta name="viewport" content="width=device-width, initial-scale=1">
<head>. width=device-width tells the browser to set the viewport width to the physical screen width (e.g. 375px for an iPhone 14). initial-scale=1 prevents automatic zooming. This is the first thing any responsive page needs.<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My Page</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- content -->
</body>
</html>
3 — Relative Units
Fixed pixel values do not flex with the screen. Relative units do — they are always calculated relative to something else, so they naturally adapt to different contexts.
Percentage — relative to the parent
A percentage width is relative to the parent element's width. This is the most fundamental fluid layout tool. A width: 100% element will always fill its container, whatever the container's width happens to be.
.container { width: 80%; } /* 80% of its parent */
.sidebar { width: 25%; }
.main { width: 75%; }
/* margin/padding % is ALWAYS relative to the parent's WIDTH
(even for top/bottom padding) */
.hero {
padding-top: 8%; /* 8% of parent width */
}
rem — relative to the root font size
rem (root em) is always relative to the <html> element's font size. By default that is 16px, so 1rem = 16px, 2rem = 32px, etc. Using rem for font sizes and spacing means everything scales up or down consistently if the user changes their browser's default font size — which is an important accessibility consideration.
em — relative to the current element's font size
em is relative to the element's own font size (or the inherited font size). This makes it useful for spacing that should scale proportionally with the text — padding on a button, for instance. It can be confusing when nested: an element at font-size: 1.5em inside another at font-size: 1.5em will be 1.5 × 1.5 = 2.25em from the root. For font sizes, prefer rem to avoid compounding.
Viewport units — relative to the viewport
- vw — 1% of the viewport width.
100vw= full viewport width. - vh — 1% of the viewport height.
100vh= full viewport height. - vmin — 1% of the smaller dimension (useful for square elements that fit any orientation).
- vmax — 1% of the larger dimension.
- dvh — "dynamic" viewport height — accounts for the browser chrome appearing/disappearing on mobile scroll. Prefer over
vhfor full-screen mobile sections.
/* rem — font sizes, global spacing, borders */
h1 { font-size: 2rem; } /* 32px at default */
body { font-size: 1rem; } /* 16px — set the baseline here */
.section { padding: 4rem 2rem; }
/* em — component-relative spacing */
.btn {
font-size: 1rem;
padding: 0.6em 1.4em; /* scales with the button's font-size */
}
/* % — fluid widths */
.sidebar { width: 30%; }
.main { width: 70%; }
/* vw/vh — viewport-relative elements */
.hero { min-height: 100dvh; } /* full-screen hero */
.modal { max-height: 90vh; } /* never taller than viewport */
.fluid-title { font-size: 5vw; } /* scales with viewport — combine with clamp() */
4 — max-width and Centering
A width: 100% container fills the screen on mobile — good. But on a 2560px monitor, a full-width line of text is unreadably long. max-width caps how wide an element can grow while still allowing it to shrink below that cap.
.container {
width: 100%; /* fills on small screens */
max-width: 1200px; /* caps on large screens */
margin: 0 auto; /* auto horizontal margins = centered */
padding: 0 1rem; /* side padding so content doesn't touch edges */
}
/* Common max-width values for different contexts */
.prose { max-width: 65ch; } /* 65 characters wide — optimal reading length */
.card { max-width: 400px; }
.page { max-width: 1200px; }
.wide-page { max-width: 1440px; }
/* min-width — prevents element from shrinking too far */
.btn {
min-width: 120px;
max-width: 300px;
}
ch unit equals the width of the "0" character in the current font. max-width: 65ch is widely considered the ideal line length for body text — comfortable for the eye to scan back to the start of the next line. Use it on article content and long-form text.This box will never exceed 600px but shrinks freely below that. Try resizing the browser.
5 — Media Queries
Media queries let you apply CSS rules only when certain conditions are true — most commonly, a specific viewport width. They are the mechanism that makes layouts shift from stacked (mobile) to side-by-side (desktop).
/* Basic syntax */
@media (min-width: 768px) {
/* rules here only apply when viewport ≥ 768px */
.sidebar { display: block; }
}
/* Range — between two sizes */
@media (min-width: 640px) and (max-width: 1023px) {
.card-grid { grid-template-columns: repeat(2, 1fr); }
}
/* Modern range syntax (2023+) */
@media (640px <= width < 1024px) {
.card-grid { grid-template-columns: repeat(2, 1fr); }
}
/* Media type — target print */
@media print {
.sidebar, .nav, .ads { display: none; }
body { font-size: 12pt; color: #000; }
}
/* Other media features */
@media (orientation: landscape) { }
@media (prefers-color-scheme: dark) { }
@media (prefers-reduced-motion: reduce) { }
6 — Mobile-First vs Desktop-First
There are two strategies for writing responsive CSS. The approach you choose determines which media query type you reach for.
| Approach | Base styles target | Media queries use | Verdict |
|---|---|---|---|
| Mobile-first | Small screens (default) | min-width — adds complexity as screens grow |
Recommended. Forces you to prioritise content; progressive enhancement; smaller CSS payload on mobile. |
| Desktop-first | Large screens (default) | max-width — strips complexity as screens shrink |
Often results in more overrides; can lead to bloated mobile CSS; harder to maintain. |
/* ── Base styles: mobile first ── */
.card-grid {
display: grid;
grid-template-columns: 1fr; /* single column on mobile */
gap: 16px;
}
/* ── Tablet: 2 columns ── */
@media (min-width: 640px) {
.card-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* ── Desktop: 3 columns ── */
@media (min-width: 1024px) {
.card-grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* ── Nav: hamburger on mobile, row on desktop ── */
.nav-links {
display: none; /* hidden on mobile */
}
@media (min-width: 768px) {
.nav-links { display: flex; gap: 24px; }
.hamburger { display: none; }
}
7 — Fluid Typography with clamp()
Instead of jumping between fixed font sizes at breakpoints, clamp() lets a value scale smoothly between a minimum and maximum as the viewport changes. This is the modern alternative to writing multiple @media rules just to adjust font sizes.
/* clamp(minimum, preferred, maximum) */
h1 {
font-size: clamp(1.5rem, 4vw, 3rem);
/* ↑min ↑scales ↑max */
/* At 400px viewport: ~1rem of 4vw = 16px → uses 1.5rem (min) */
/* At 800px viewport: 4vw = 32px → scales between min and max */
/* At 1200px viewport: 4vw = 48px → uses 3rem (max) */
}
/* Practical values for common elements */
h1 { font-size: clamp(1.75rem, 4vw, 3rem); }
h2 { font-size: clamp(1.35rem, 3vw, 2.25rem); }
body { font-size: clamp(0.9rem, 1.5vw, 1rem); }
/* Also works for spacing */
.section {
padding: clamp(2rem, 8vw, 8rem) clamp(1rem, 4vw, 4rem);
}
vw value. The formula clamp(MIN, VIEWPORT_UNIT, MAX) gives you a smooth scale between the two extremes — no JavaScript required. The clamp() function is supported in all modern browsers.8 — Responsive Images
Images without explicit responsive handling will overflow their containers on small screens. The baseline fix is a single CSS rule applied globally.
/* Add this to every project's global styles */
img,
video,
iframe {
max-width: 100%;
height: auto; /* maintains aspect ratio */
display: block; /* removes bottom whitespace gap (images are inline by default) */
}
/* aspect-ratio — reserve space before image loads */
.card-image {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover; /* crop to fill, no distortion */
}
/* HTML srcset — serve different image sizes at different widths */
<!-- Browser picks the most appropriate file -->
<img
src="photo-800.jpg"
srcset="photo-400.jpg 400w,
photo-800.jpg 800w,
photo-1600.jpg 1600w"
sizes="(max-width: 640px) 100vw,
(max-width: 1024px) 50vw,
800px"
alt="Description"
>
sizes attribute tells the browser how wide the image will be at each breakpoint so it can pick the right source before downloading it.9 — Putting It Together
/* ─── Reset + baseline ─── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
img, video { max-width: 100%; height: auto; display: block; }
:root {
--content-width: 1200px;
--side-pad: clamp(1rem, 4vw, 3rem);
}
/* ─── Container ─── */
.container {
width: 100%;
max-width: var(--content-width);
margin: 0 auto;
padding: 0 var(--side-pad);
}
/* ─── Nav: hamburger on mobile → row on desktop ─── */
.site-nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem var(--side-pad);
}
.nav-links { display: none; }
.hamburger { display: block; }
@media (min-width: 768px) {
.nav-links { display: flex; gap: 1.5rem; list-style: none; }
.hamburger { display: none; }
}
/* ─── Card grid: 1 → 2 → 3 columns ─── */
.card-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
@media (min-width: 640px) { .card-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .card-grid { grid-template-columns: repeat(3, 1fr); } }
/* ─── Sidebar layout: stacked → side-by-side ─── */
.page-layout {
display: grid;
gap: 2rem;
}
@media (min-width: 900px) {
.page-layout {
grid-template-columns: 240px 1fr;
}
}
10 — Quick Reference
| Tool | Syntax / Example | Purpose |
|---|---|---|
| Foundation | ||
| Viewport meta tag | <meta name="viewport" content="width=device-width, initial-scale=1"> | Required in every responsive page's <head> |
| Global image fix | img { max-width: 100%; height: auto; } | Prevents image overflow on small screens |
| Centered container | max-width: 1200px; margin: 0 auto; padding: 0 1rem; | Caps width, centers horizontally, adds side breathing room |
| Units | ||
% | width: 50% | Relative to parent width |
rem | font-size: 1.25rem | Relative to root (html) font size. Best for font sizes and global spacing. |
em | padding: 0.5em 1em | Relative to element's own font size. Good for component-local spacing. |
vw / vh | min-height: 100dvh | Viewport width / height. Use dvh for mobile hero sections. |
clamp() | font-size: clamp(1rem, 3vw, 2rem) | Fluid value that scales between a min and max |
| Media queries | ||
| Min-width (mobile-first) | @media (min-width: 768px) { } | Rules apply at 768px and above |
| Max-width (desktop-first) | @media (max-width: 767px) { } | Rules apply at 767px and below |
| Range | @media (640px <= width < 1024px) { } | Rules apply between two sizes (modern syntax) |
@media print { } | Applied when page is printed | |
| Dark mode | @media (prefers-color-scheme: dark) { } | Adapts to user's OS colour scheme |
✏️ Exercises
These exercises require a browser to test — media queries can't be demonstrated inline in a static snippet. Resize the browser window (or use DevTools device emulation: F12 → the device icon) to verify your breakpoints trigger correctly.
max-width and have horizontal padding so the content doesn't touch the screen edges.justify-content: space-between. The links are display: none by default and display: flex inside a min-width: 768px media query. The hamburger is the reverse.<nav class="site-nav">
<div class="nav-container">
<a href="/" class="nav-logo">MySite</a>
<button class="hamburger" aria-label="Menu">☰</button>
<ul class="nav-links">
<li><a href="#">About</a></li>
<li><a href="#">Work</a></li>
<li><a href="#">Contact</a></li>
</ul>
</div>
</nav>
.site-nav {
background: #1a1d27;
border-bottom: 1px solid #2a2d3a;
}
.nav-container {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto;
padding: 1rem 1.5rem;
}
.nav-logo {
font-weight: 700;
font-size: 1.2rem;
color: #fff;
text-decoration: none;
}
/* Mobile: hide links, show hamburger */
.nav-links { display: none; list-style: none; gap: 1.5rem; }
.hamburger { display: block; background: none; border: none;
color: #fff; font-size: 1.5rem; cursor: pointer; }
/* Tablet+: show links, hide hamburger */
@media (min-width: 768px) {
.nav-links {
display: flex;
align-items: center;
}
.nav-links a {
color: #a0a4b8;
text-decoration: none;
transition: color 0.2s;
}
.nav-links a:hover { color: #fff; }
.hamburger { display: none; }
}
grid-template-columns: 1fr as the base, then change the column count in two min-width media queries. Wrap the grid in a .container with max-width: 1200px; margin: 0 auto; padding: 0 1rem..container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
}
.card-grid {
display: grid;
grid-template-columns: 1fr; /* mobile: 1 column */
gap: 1.25rem;
}
@media (min-width: 600px) {
.card-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
.card-grid { grid-template-columns: repeat(3, 1fr); }
}
.card {
background: #1a1d27;
border: 1px solid #2a2d3a;
border-radius:8px;
padding: 1.5rem;
}
.card h3 {
font-size: 1.1rem;
color: #fff;
margin-bottom:0.5rem;
}
.card p {
color: #a0a4b8;
font-size: 0.9rem;
line-height: 1.6;
}
dvh. The heading should use clamp() to scale from 2rem at minimum to 4.5rem at maximum, passing through a viewport-relative value. The subheading and button text should also use clamp(). Include a background image (or a gradient placeholder) that uses object-fit: cover. The hero text should sit over the background using the positioning techniques from Chapter 7.position: relative; min-height: 100dvh; overflow: hidden. The image is position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover. The text content has position: relative; z-index: 1 to sit above the image.<section class="hero">
<img class="hero-bg" src="your-image.jpg" alt="" aria-hidden="true">
<div class="hero-content">
<h1>Make something great.</h1>
<p>Beautiful work, delivered thoughtfully.</p>
<a href="#" class="hero-cta">See our work</a>
</div>
</section>
.hero {
position: relative;
min-height: 100dvh;
overflow: hidden;
display: flex;
align-items:center;
}
.hero-bg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 0;
/* Fallback if no image: */
background: linear-gradient(135deg, #0d1117 0%, #1a1d27 100%);
}
/* Darken the background for text legibility */
.hero::after {
content: '';
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1;
}
.hero-content {
position: relative;
z-index: 2;
max-width: 800px;
margin: 0 auto;
padding: clamp(2rem, 8vw, 6rem) clamp(1rem, 4vw, 3rem);
text-align: center;
}
.hero-content h1 {
font-size: clamp(2rem, 6vw, 4.5rem);
line-height: 1.1;
color: #fff;
margin-bottom: 1rem;
}
.hero-content p {
font-size: clamp(1rem, 2vw, 1.35rem);
color: rgba(255, 255, 255, 0.8);
margin-bottom: 2rem;
}
.hero-cta {
display: inline-block;
background: #4f8ef7;
color: #fff;
padding: 0.75rem 2rem;
border-radius: 6px;
font-size: clamp(0.9rem, 1.5vw, 1rem);
font-weight: 700;
text-decoration: none;
transition: background 0.2s, transform 0.2s;
}
.hero-cta:hover {
background: #3a7ae0;
transform: translateY(-2px);
}
max-width of 65ch to limit line length. The article's heading should use clamp(). The sidebar should contain a "Related posts" section. The entire layout should be wrapped in a centered container with a max-width and fluid side padding using clamp().grid-template-columns: 1fr on mobile. At 900px, switch to grid-template-columns: 2fr 1fr. The prose max-width: 65ch applies inside the article, not the grid column — the column itself is fluid.<div class="page-container">
<h1 class="page-title">My Blog</h1>
<div class="blog-layout">
<article class="article">
<h2>Getting started with CSS Grid</h2>
<p>Article content here...</p>
</article>
<aside class="sidebar">
<h3>Related posts</h3>
<ul>
<li><a href="#">Flexbox vs Grid</a></li>
<li><a href="#">Responsive typography</a></li>
</ul>
</aside>
</div>
</div>
.page-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem clamp(1rem, 4vw, 3rem);
}
.page-title {
font-size: clamp(1.5rem, 4vw, 2.5rem);
margin-bottom: 2rem;
}
/* ── Mobile: single column ── */
.blog-layout {
display: grid;
gap: 2.5rem;
}
/* ── Desktop: two-column ── */
@media (min-width: 900px) {
.blog-layout {
grid-template-columns: 2fr 1fr;
align-items: start;
}
}
.article h2 {
font-size: clamp(1.25rem, 2.5vw, 1.75rem);
margin-bottom: 1rem;
}
.article p {
max-width: 65ch; /* optimal reading width */
line-height: 1.75;
color: #a0a4b8;
}
.sidebar {
background: #1a1d27;
border: 1px solid #2a2d3a;
border-radius:8px;
padding: 1.25rem;
}
.sidebar h3 {
margin-bottom:0.75rem;
font-size: 1rem;
color: #fff;
}
.sidebar ul { list-style: none; padding: 0; }
.sidebar li { margin-bottom: 0.5rem; }
.sidebar a { color: #7ab8ff; font-size: 0.9rem; }
.sidebar a:hover { color: #fff; }