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.

The same content rendered at three viewport widths
mobile
Hero
section
Card
Card
Card
~375px
tablet
Logo About Work Contact
Hero section
Card A
Card B
~768px
desktop
Logo About Work Services Blog Contact
Hero section — full width
Card A
Card B
Card C
~1280px

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.

🌐 HTML — The essential viewport meta tag
<meta name="viewport" content="width=device-width, initial-scale=1">
This goes inside <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.
The single most common responsive design mistake is forgetting this tag. You can write perfect media queries but if this meta tag is absent, the mobile browser renders at ~980px wide and scales down — your breakpoints will never trigger. Always include it.
🌐 HTML — Complete responsive page boilerplate
<!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.

🎨 CSS — Percentage widths
.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 vh for full-screen mobile sections.
Unit comparison — showing how wide each unit resolves in this context
200px
Fixed — never changes
60%
% of parent width
6rem
6 × 16px = 96px
40vw
40% of viewport
🎨 CSS — When to use each unit
/* 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.

🎨 CSS — The classic centered container pattern
.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; }
The 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.
max-width: 600px with margin: 0 auto — centered, capped
Outer container — full width
max-width: 600px; margin: 0 auto
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).

Mobile
<640
Tablet
640–1024
Desktop
1024–1440
Wide
>1440
base styles @media (min-width: 640px) @media (min-width: 1024px) @media (min-width: 1440px)
🎨 CSS — Media query syntax and common patterns
/* 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.

ApproachBase styles targetMedia queries useVerdict
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.
🎨 CSS — Mobile-first pattern (recommended)
/* ── 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; } }
💡 Common breakpoint values — there is no universal standard, but these are widely used and broadly match device categories: 480px (small phones), 640px (phones landscape / small tablets), 768px (tablets), 1024px (small desktops / tablets landscape), 1280px (desktops), 1536px (large monitors). Frameworks like Tailwind CSS use these exact values. Design to the content, not specific devices — your breakpoints should be where the design starts to break.

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.

🎨 CSS — clamp() for fluid 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); }
The middle value (the "preferred") is usually a 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.
Live — this heading and text use clamp() and scale with the viewport
clamp(1.1rem, 3vw, 2rem) for the heading · clamp(0.85rem, 1.5vw, 1rem) for body
This heading scales fluidly with the viewport width
This paragraph text also scales within its bounds — never too small on mobile, never too large on a wide monitor. Resize your browser window to see the effect in action.

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.

✗ No responsive CSS
🖼️
img { width: 200px; }
Overflows small containers — requires horizontal scroll
✓ Responsive image
🖼️
img { max-width: 100%; }
Shrinks to fit any container — never overflows
🎨 CSS — The global responsive image rule
/* 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 */
🌐 HTML — srcset for resolution switching
<!-- 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" >
Mobile devices on slow connections download the small file (400w), desktops get the large one (1600w). The 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

🎨 CSS — A complete mobile-first page skeleton
/* ─── 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

ToolSyntax / ExamplePurpose
Foundation
Viewport meta tag<meta name="viewport" content="width=device-width, initial-scale=1">Required in every responsive page's <head>
Global image fiximg { max-width: 100%; height: auto; }Prevents image overflow on small screens
Centered containermax-width: 1200px; margin: 0 auto; padding: 0 1rem;Caps width, centers horizontally, adds side breathing room
Units
%width: 50%Relative to parent width
remfont-size: 1.25remRelative to root (html) font size. Best for font sizes and global spacing.
empadding: 0.5em 1emRelative to element's own font size. Good for component-local spacing.
vw / vhmin-height: 100dvhViewport 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)
Print@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.

Exercise 1
Build a responsive navigation bar. On mobile (<768px), show a logo on the left and a hamburger icon (☰) on the right, with the links hidden. At 768px and above, hide the hamburger and show the links as a horizontal row. The container should be centered with a max-width and have horizontal padding so the content doesn't touch the screen edges.
Hint: make the nav a flex container with 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.
HTML
<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>
CSS
.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; } }
Exercise 2
Create a responsive card grid using only CSS (no JavaScript). On screens below 600px, cards stack in a single column. Between 600px and 1024px they display in two columns. Above 1024px they display in three columns. Each card should have a consistent appearance: rounded corners, a border, a title, and a short description. The entire grid should be centered on the page and not exceed 1200px wide.
Hint: use a CSS Grid container with 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.
CSS
.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; }
Exercise 3
Build a hero section with fluid typography and a responsive image. The hero should be full-viewport-height using 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.
Hint: the hero container is 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.
HTML
<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>
CSS
.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); }
Exercise 4
Build a complete two-column blog layout that is fully responsive. On mobile: the page title, article, and sidebar stack vertically in that order. On desktop (≥900px): the article takes up the left two-thirds and the sidebar occupies the right third, side by side. The prose in the article should have a 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().
Hint: the two-column layout uses a CSS Grid container with 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.
HTML
<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>
CSS
.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; }