Responsive Design & Accessibility: The Complete Guide to Building for Everyone
You built a beautiful blog with Grid, Flexbox, and modern CSS. It looks great on your laptop. But here's the reality: over 60% of web traffic comes from mobile devices, and 1 in 6 people worldwide live with a disability that affects how they use the web.
A website that only works on a 1440px monitor is a broken website. A website that looks good but can't be navigated with a keyboard or understood by a screen reader is exclusionary by design.
This module fixes both. We'll take your blog from Module 3, make it fully responsive using a mobile-first approach, and ensure it meets WCAG 2.2 accessibility standards. By the end, your site will work on a $50 Android phone, a 4K monitor, and a screen reader — without breaking a sweat.
Part 1: Mobile-First vs. Desktop-First — The Mindset Shift
Most beginners write CSS for their laptop screen first, then add media queries to "fix" mobile. This creates bloated, fragile code.
Mobile-First: Start Small, Scale Up
Write your base styles for the smallest screen. Then use min-width media queries to enhance for larger screens.
1/* Base: Mobile (default, no media query needed) */ 2.card { 3 padding: 1rem; 4 font-size: 1rem; 5} 6 7/* Tablet */ 8@media (min-width: 640px) { 9 .card { 10 padding: 1.5rem; 11 display: grid; 12 grid-template-columns: 200px 1fr; 13 gap: 1.5rem; 14 } 15} 16 17/* Desktop */ 18@media (min-width: 1024px) { 19 .card { 20 padding: 2rem; 21 } 22}
Why this works:
- Mobile styles are the simplest. They load first for everyone.
- You add complexity for larger screens, rather than overriding desktop complexity for mobile.
- Your CSS file is smaller and easier to maintain.
Desktop-First: The Trap
1/* Desktop first — DON'T DO THIS */ 2.card { 3 display: grid; 4 grid-template-columns: repeat(3, 1fr); /* Desktop */ 5 padding: 2rem; 6} 7 8/* Now you have to undo all that for mobile */ 9@media (max-width: 639px) { 10 .card { 11 display: block; /* Undoing grid */ 12 padding: 1rem; /* Undoing padding */ 13 } 14}
Every max-width query is an override. Overrides add specificity debt. Mobile-first avoids this entirely.
The Rule: If you find yourself writing
display: blockinside amax-widthquery to undo a desktop layout, you're doing it backwards.
Part 2: Media Queries — Breakpoints Based on Content, Not Devices
Don't copy breakpoints from Bootstrap (576px, 768px, 992px, 1200px). Those were based on 2014 device widths. Today's devices range from 320px foldables to 3440px ultrawides.
The Content-Based Breakpoint Method
Add a breakpoint only when your content breaks.
1/* Step 1: Build the mobile layout */ 2.gallery { 3 display: grid; 4 gap: 1rem; 5} 6 7/* Step 2: Resize your browser until the cards look too narrow */ 8/* Step 3: That's your breakpoint */ 9@media (min-width: 480px) { 10 .gallery { 11 grid-template-columns: repeat(2, 1fr); 12 } 13} 14 15/* Step 4: Keep resizing. When 2 columns look too wide... */ 16@media (min-width: 768px) { 17 .gallery { 18 grid-template-columns: repeat(3, 1fr); 19 } 20}
Modern Media Query Features
1/* Range syntax (cleaner than min/max) */ 2@media (width >= 640px) { /* ... */ } 3@media (640px <= width < 1024px) { /* tablet only */ } 4 5/* Height matters too */ 6@media (min-height: 600px) { 7 .hero { min-height: 60vh; } 8} 9 10/* Aspect ratio */ 11@media (aspect-ratio >= 16/9) { 12 .video-container { aspect-ratio: 16/9; } 13} 14 15/* Pointer precision (touch vs mouse) */ 16@media (pointer: coarse) { 17 .button { min-height: 44px; } /* Larger touch targets */ 18} 19 20@media (pointer: fine) { 21 .button:hover { transform: translateY(-2px); } 22} 23 24/* Hover capability */ 25@media (hover: hover) { 26 .card:hover { box-shadow: 0 8px 30px rgba(0,0,0,0.12); } 27} 28 29/* Reduced motion (CRITICAL for accessibility) */ 30@media (prefers-reduced-motion: reduce) { 31 *, 32 *::before, 33 *::after { 34 animation-duration: 0.01ms !important; 35 animation-iteration-count: 1 !important; 36 transition-duration: 0.01ms !important; 37 } 38 39 .reading-progress { display: none; } 40} 41 42/* Dark mode preference from OS */ 43@media (prefers-color-scheme: dark) { 44 :root { 45 --color-bg: #0f172a; 46 --color-text: #f1f5f9; 47 } 48}
Always respect
prefers-reduced-motion. For people with vestibular disorders, animations can cause nausea, dizziness, and disorientation. Your site must work without motion.
Part 3: Responsive Images — Stop Shipping Desktop Images to Phones
A 2000px wide hero image weighs 400KB. On a 375px phone, that's 400KB of wasted data. Responsive images fix this.
srcset and sizes
1<img 2 srcset=" 3 photo-400.jpg 400w, 4 photo-800.jpg 800w, 5 photo-1200.jpg 1200w, 6 photo-1600.jpg 1600w 7 " 8 sizes=" 9 (max-width: 640px) 100vw, 10 (max-width: 1024px) 50vw, 11 33vw 12 " 13 src="photo-800.jpg" 14 alt="A developer working at a standing desk with dual monitors" 15 width="1600" 16 height="1067" 17 loading="lazy" 18 decoding="async" 19>
How it works:
srcsetlists available image widths (wdescriptor)sizestells the browser how big the image will be at different breakpoints- The browser picks the smallest image that covers the rendered size
The <picture> Element for Art Direction
Sometimes you need a completely different image for mobile vs desktop — not just a smaller version.
1<picture> 2 <!-- Mobile: square crop --> 3 <source 4 media="(max-width: 639px)" 5 srcset="hero-mobile.jpg" 6 width="600" 7 height="600"> 8 9 <!-- Desktop: wide banner --> 10 <source 11 media="(min-width: 640px)" 12 srcset="hero-desktop.jpg" 13 width="1600" 14 height="600"> 15 16 <!-- Fallback --> 17 <img 18 src="hero-desktop.jpg" 19 alt="Team collaborating in a modern office space" 20 width="1600" 21 height="600" 22 loading="eager"> 23</picture>
SVG for Icons and Logos
SVGs scale infinitely, stay sharp, and are tiny in file size. Use them for icons, logos, and simple illustrations.
1<!-- Inline SVG (styleable with CSS) --> 2<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"> 3 <path d="M12 2L2 7l10 5 10-5-10-5z" stroke="currentColor" stroke-width="2"/> 4 <path d="M2 17l10 5 10-5" stroke="currentColor" stroke-width="2"/> 5</svg> 6 7<!-- External SVG (cached, but harder to style) --> 8<img src="icon.svg" alt="" width="24" height="24">
Always set
widthandheighton images. This reserves space before the image loads, preventing Cumulative Layout Shift (CLS) — a Core Web Vital that affects SEO rankings.
Part 4: The Viewport Meta Tag and Mobile Rendering
Without the viewport meta tag, mobile browsers scale your desktop site down to fit the screen. Text becomes unreadable. Users have to pinch-zoom.
1<!-- Correct --> 2<meta name="viewport" content="width=device-width, initial-scale=1.0"> 3 4<!-- Common mistake: disabling zoom (bad for accessibility) --> 5<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> 6<!-- DON'T DO THIS — it prevents users with low vision from zooming -->
Common Mobile Rendering Bugs
Bug 1: 100vh includes the browser chrome
On iOS Safari, 100vh includes the bottom navigation bar, causing content to be hidden.
1/* Fix: Use dvh (dynamic viewport height) */ 2.hero { 3 min-height: 100dvh; /* Adjusts as browser chrome shows/hides */ 4} 5 6/* Fallback for older browsers */ 7@supports not (height: 100dvh) { 8 .hero { min-height: 100vh; } 9}
Bug 2: Horizontal scroll from overflow Something is wider than the viewport.
1/* Nuclear option to find the culprit */ 2* { 3 outline: 1px solid red; /* Remove after debugging */ 4} 5 6/* Common fixes */ 7img, video, iframe { 8 max-width: 100%; 9 height: auto; 10} 11 12/* Prevent word overflow */ 13p, h1, h2, h3 { 14 overflow-wrap: break-word; 15 hyphens: auto; 16}
Bug 3: Font size inflation on mobile iOS Safari inflates font sizes in landscape mode.
1html { 2 -webkit-text-size-adjust: 100%; /* Prevent iOS font inflation */ 3}
Part 5: Accessibility Fundamentals — WCAG 2.2
The Web Content Accessibility Guidelines (WCAG) 2.2 are the international standard. You don't need to memorize all 86 success criteria. Focus on these four principles: POUR.
| Principle | Meaning | Example |
|---|---|---|
| Perceivable | Information must be presentable in ways users can perceive | Alt text for images, captions for videos |
| Operable | Interface components must be operable by all users | Keyboard navigation, enough time to read |
| Understandable | Information and UI operation must be understandable | Clear labels, error prevention |
| Robust | Content must work with current and future assistive tech | Valid HTML, ARIA used correctly |
Color Contrast Ratios
Text must have sufficient contrast against its background.
| Element | Minimum Ratio | Enhanced Ratio |
|---|---|---|
| Normal text (< 18px) | 4.5:1 | 7:1 |
| Large text (≥ 18px bold or ≥ 24px) | 3:1 | 4.5:1 |
| UI components (buttons, form borders) | 3:1 | — |
Testing tools:
- Chrome DevTools → Elements → Styles → Color swatch → Contrast ratio
- WebAIM Contrast Checker (webaim.org/resources/contrastchecker)
- Stark plugin for Figma
1/* Bad: fails contrast */ 2.button-secondary { 3 color: #999; /* Way too light on white */ 4 border: 1px solid #ccc; 5} 6 7/* Good: passes WCAG AA */ 8.button-secondary { 9 color: #475569; /* 7.2:1 on white */ 10 border: 1px solid #94a3b8; /* 3.1:1 on white */ 11}
Focus Indicators — Never Remove Them
1/* NEVER do this */ 2*:focus { outline: none; } /* You just made your site unusable with a keyboard */ 3 4/* DO this: style the focus to match your design */ 5a:focus-visible, 6button:focus-visible, 7input:focus-visible, 8textarea:focus-visible, 9select:focus-visible { 10 outline: 3px solid var(--color-primary); 11 outline-offset: 3px; 12 border-radius: 2px; 13} 14 15/* Remove default focus ring only if you're replacing it */ 16a:focus:not(:focus-visible) { 17 outline: none; 18}
Focus-visible vs Focus:
:focusapplies when an element is focused by any means (mouse, keyboard, script).:focus-visibleonly applies when focused via keyboard or assistive tech — preventing ugly rings on mouse clicks while keeping them for keyboard users.
Keyboard Navigation Checklist
Test your entire site using only the Tab, Enter, Space, Arrow, and Escape keys.
| Key | Expected Behavior |
|---|---|
Tab | Move focus to next interactive element |
Shift + Tab | Move focus to previous interactive element |
Enter | Activate links and buttons |
Space | Toggle checkboxes, press buttons |
Arrow keys | Navigate within menus, tabs, radio groups |
Escape | Close modals, dropdowns, popovers |
Part 6: ARIA — When and How to Use It
ARIA (Accessible Rich Internet Applications) adds semantic information that HTML alone can't express. But the first rule of ARIA is: don't use ARIA if you can use HTML instead.
Native HTML Beats ARIA
| Don't Do This | Do This Instead |
|---|---|
<div role="button"> | <button> |
<span role="link"> | <a href="..."> |
<div role="heading" aria-level="2"> | <h2> |
<div role="list"> | <ul> or <ol> |
<div role="listitem"> | <li> |
When ARIA Is Actually Needed
1. Landmark Labels
1<!-- Multiple navs need labels so screen reader users know which is which --> 2<nav aria-label="Main"> 3 <!-- primary links --> 4</nav> 5 6<nav aria-label="Footer"> 7 <!-- footer links --> 8</nav>
2. Dynamic Content
1<!-- Live region: announces updates without moving focus --> 2<div aria-live="polite" aria-atomic="true" class="toast-container"> 3 <!-- Toasts injected here will be announced --> 4</div> 5 6<!-- aria-live values: 7 off: Don't announce (default) 8 polite: Announce when user is idle 9 assertive: Interrupt immediately (use sparingly) 10-->
3. Expanded/Collapsed States
1<button 2 aria-expanded="false" 3 aria-controls="menu-panel" 4 id="menu-btn"> 5 Menu 6</button> 7<div id="menu-panel" role="region" aria-labelledby="menu-btn" hidden> 8 <!-- menu items --> 9</div>
4. Current Page Indication
1<!-- We already did this in Module 1 --> 2<nav> 3 <a href="index.html" aria-current="page">Home</a> 4 <a href="about.html">About</a> 5</nav>
5. Labels When Text Is Visual-Only
1<!-- Icon button needs accessible name --> 2<button aria-label="Close dialog"> 3 <svg aria-hidden="true">...</svg> 4</button> 5 6<!-- Or use aria-labelledby to reference visible text --> 7<button aria-labelledby="save-label"> 8 <svg aria-hidden="true">...</svg> 9 <span id="save-label">Save changes</span> 10</button>
ARIA Roles Reference
1<header> <!-- implicit role: banner --> 2<nav> <!-- implicit role: navigation --> 3<main> <!-- implicit role: main --> 4<aside> <!-- implicit role: complementary --> 5<footer> <!-- implicit role: contentinfo --> 6<form> <!-- implicit role: form --> 7<section> <!-- implicit role: region (if labeled) --> 8<article> <!-- implicit role: article -->
You rarely need explicit
roleattributes if you use semantic HTML5 elements. Save ARIA roles for custom components (tabs, modals, carousels) that don't have native HTML equivalents.
Part 7: Testing Accessibility — Three Methods
Method 1: Automated Testing (Lighthouse + axe)
Lighthouse (built into Chrome DevTools):
- Open DevTools → Lighthouse
- Check "Accessibility"
- Run audit
- Fix every issue marked as "Failing"
axe DevTools (browser extension):
- More thorough than Lighthouse
- Catches issues like missing heading levels, improper ARIA usage
- Free version catches most critical issues
Method 2: Manual Keyboard Test
- Unplug your mouse. Seriously.
- Press
Tabto navigate through your entire site. - Ask yourself:
- Can I reach every interactive element?
- Is the focus order logical (top to bottom, left to right)?
- Can I see where focus is at all times?
- Can I activate every button and link with
EnterorSpace? - Can I fill out and submit the form without touching a mouse?
Method 3: Screen Reader Test
macOS: Cmd + F5 turns on VoiceOver
Windows: Install NVDA (free)
Linux: Install Orca
Navigate by:
- Headings:
Ctrl + Option + Cmd + H(VoiceOver) - Landmarks:
Ctrl + Option + Uthen arrow keys - Links:
Ctrl + Option + Cmd + L
Listen for:
- Image descriptions (alt text)
- Form labels
- Heading hierarchy
- Landmark announcements ("Main", "Navigation", "Complementary")
Part 8: The Mini-Project — Fully Responsive & Accessible Blog
Here's the complete, updated blog CSS and HTML that implements everything from this module.
Updated styles.css (Mobile-First, Accessible)
1/* ============================================ 2 RESET & CUSTOM PROPERTIES 3 ============================================ */ 4*, 5*::before, 6*::after { 7 box-sizing: border-box; 8 margin: 0; 9 padding: 0; 10} 11 12:root { 13 /* Brand */ 14 --color-primary: #2563eb; 15 --color-primary-dark: #1d4ed8; 16 --color-primary-light: #60a5fa; 17 18 /* Light mode (default) */ 19 --color-bg: #ffffff; 20 --color-surface: #f8fafc; 21 --color-surface-elevated: #ffffff; 22 --color-text: #0f172a; 23 --color-text-muted: #64748b; 24 --color-border: #e2e8f0; 25 --shadow-color: rgb(15 23 42 / 0.08); 26 27 /* Typography */ 28 --font-body: 'Inter', system-ui, -apple-system, sans-serif; 29 --text-xs: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem); 30 --text-sm: clamp(0.875rem, 0.8rem + 0.35vw, 1rem); 31 --text-base: clamp(1rem, 0.9rem + 0.5vw, 1.125rem); 32 --text-lg: clamp(1.125rem, 1rem + 0.65vw, 1.35rem); 33 --text-xl: clamp(1.35rem, 1.1rem + 1.2vw, 1.75rem); 34 --text-2xl: clamp(1.75rem, 1.3rem + 2.2vw, 2.5rem); 35 36 /* Spacing */ 37 --space-xs: 0.5rem; 38 --space-sm: 1rem; 39 --space-md: 1.5rem; 40 --space-lg: 2.5rem; 41 --space-xl: 4rem; 42 43 /* Motion */ 44 --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); 45 --transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1); 46} 47 48/* Dark mode via OS preference */ 49@media (prefers-color-scheme: dark) { 50 :root { 51 --color-bg: #0f172a; 52 --color-surface: #1e293b; 53 --color-surface-elevated: #334155; 54 --color-text: #f1f5f9; 55 --color-text-muted: #94a3b8; 56 --color-border: #475569; 57 --shadow-color: rgb(0 0 0 / 0.3); 58 } 59} 60 61/* Dark mode via toggle (from Module 4) */ 62:root:has(#theme-toggle:checked) { 63 --color-bg: #0f172a; 64 --color-surface: #1e293b; 65 --color-surface-elevated: #334155; 66 --color-text: #f1f5f9; 67 --color-text-muted: #94a3b8; 68 --color-border: #475569; 69 --shadow-color: rgb(0 0 0 / 0.3); 70} 71 72/* Respect reduced motion */ 73@media (prefers-reduced-motion: reduce) { 74 *, 75 *::before, 76 *::after { 77 animation-duration: 0.01ms !important; 78 animation-iteration-count: 1 !important; 79 transition-duration: 0.01ms !important; 80 scroll-behavior: auto !important; 81 } 82} 83 84html { 85 font-size: 100%; 86 -webkit-text-size-adjust: 100%; 87 scroll-behavior: smooth; 88} 89 90body { 91 font-family: var(--font-body); 92 font-size: var(--text-base); 93 line-height: 1.6; 94 color: var(--color-text); 95 background-color: var(--color-bg); 96 transition: background-color var(--transition-base), color var(--transition-base); 97} 98 99/* ============================================ 100 SKIP LINK 101 ============================================ */ 102.skip-link { 103 position: absolute; 104 top: -50px; 105 left: 0; 106 background: var(--color-primary); 107 color: #ffffff; 108 padding: 0.75rem 1.25rem; 109 z-index: 10000; 110 text-decoration: none; 111 font-weight: 600; 112 border-radius: 0 0 8px 0; 113 transition: top var(--transition-fast); 114} 115 116.skip-link:focus { 117 top: 0; 118} 119 120/* ============================================ 121 PAGE LAYOUT — MOBILE FIRST 122 ============================================ */ 123.page-layout { 124 display: grid; 125 grid-template-columns: 1fr; 126 grid-template-areas: 127 "header" 128 "main" 129 "footer"; 130 min-height: 100dvh; 131 gap: 0; 132} 133 134/* Tablet and up: sidebar appears */ 135@media (min-width: 900px) { 136 .page-layout { 137 grid-template-columns: 260px 1fr; 138 grid-template-areas: 139 "header header" 140 "sidebar main" 141 "footer footer"; 142 gap: 0 2rem; 143 max-width: 1200px; 144 margin: 0 auto; 145 padding: 0 1.5rem; 146 } 147} 148 149.page-layout > header { grid-area: header; } 150.page-layout > aside { grid-area: sidebar; } 151.page-layout > main { grid-area: main; } 152.page-layout > footer { grid-area: footer; } 153 154/* ============================================ 155 HEADER — STICKY, RESPONSIVE 156 ============================================ */ 157header { 158 position: sticky; 159 top: 0; 160 z-index: 100; 161 background: color-mix(in oklch, var(--color-surface) 92%, transparent); 162 backdrop-filter: blur(12px); 163 border-bottom: 1px solid var(--color-border); 164} 165 166.header-inner { 167 display: flex; 168 flex-wrap: wrap; 169 align-items: center; 170 gap: var(--space-sm) var(--space-md); 171 padding: var(--space-sm) var(--space-md); 172 max-width: 1200px; 173 margin: 0 auto; 174} 175 176header h1 { 177 font-size: var(--text-xl); 178 font-weight: 700; 179 line-height: 1.2; 180 margin: 0; 181} 182 183header h1 a { 184 color: var(--color-text); 185 text-decoration: none; 186} 187 188header h1 a:focus-visible { 189 outline: 3px solid var(--color-primary); 190 outline-offset: 4px; 191 border-radius: 4px; 192} 193 194header > .header-inner > p { 195 color: var(--color-text-muted); 196 font-size: var(--text-sm); 197 margin: 0; 198 flex-basis: 100%; 199} 200 201/* Navigation */ 202header nav { 203 margin-left: auto; 204 width: 100%; 205} 206 207header nav ul { 208 list-style: none; 209 display: flex; 210 gap: var(--space-md); 211 padding: 0; 212 margin: 0; 213 overflow-x: auto; 214 -webkit-overflow-scrolling: touch; 215 scrollbar-width: none; /* Hide scrollbar on mobile */ 216} 217 218header nav ul::-webkit-scrollbar { 219 display: none; 220} 221 222header nav a { 223 display: block; 224 color: var(--color-text-muted); 225 text-decoration: none; 226 font-weight: 500; 227 font-size: var(--text-sm); 228 padding: 0.5rem 0; 229 border-bottom: 2px solid transparent; 230 white-space: nowrap; 231 transition: color var(--transition-fast), border-color var(--transition-fast); 232} 233 234header nav a:hover { 235 color: var(--color-primary); 236} 237 238header nav a:focus-visible { 239 outline: 3px solid var(--color-primary); 240 outline-offset: 4px; 241 border-radius: 4px; 242 border-bottom-color: transparent; 243} 244 245header nav a[aria-current="page"] { 246 color: var(--color-primary); 247 border-bottom-color: var(--color-primary); 248 font-weight: 600; 249} 250 251/* Tablet and up: nav beside logo */ 252@media (min-width: 640px) { 253 header nav { 254 width: auto; 255 } 256 257 header > .header-inner > p { 258 flex-basis: auto; 259 } 260} 261 262/* ============================================ 263 SIDEBAR 264 ============================================ */ 265aside { 266 padding: var(--space-md); 267 border-top: 1px solid var(--color-border); 268 order: 1; /* Moves below main on mobile */ 269} 270 271@media (min-width: 900px) { 272 aside { 273 order: 0; 274 padding: var(--space-lg) 0; 275 border-top: none; 276 } 277} 278 279.widget { 280 background: var(--color-surface-elevated); 281 border-radius: 12px; 282 padding: var(--space-md); 283 margin-bottom: var(--space-md); 284 border: 1px solid var(--color-border); 285} 286 287.widget h2 { 288 font-size: var(--text-base); 289 font-weight: 600; 290 margin-bottom: var(--space-sm); 291 padding-bottom: var(--space-xs); 292 border-bottom: 2px solid var(--color-primary); 293} 294 295.widget ul { 296 list-style: none; 297 padding: 0; 298 margin: 0; 299} 300 301.widget li { 302 margin-bottom: var(--space-xs); 303} 304 305.widget a { 306 display: flex; 307 align-items: center; 308 gap: 0.5rem; 309 color: var(--color-text); 310 text-decoration: none; 311 font-size: var(--text-sm); 312 padding: 0.375rem 0; 313 border-radius: 4px; 314 transition: color var(--transition-fast), padding var(--transition-fast); 315} 316 317.widget a:hover { 318 color: var(--color-primary); 319 padding-left: 0.25rem; 320} 321 322.widget a:focus-visible { 323 outline: 2px solid var(--color-primary); 324 outline-offset: 2px; 325} 326 327.widget a::before { 328 content: "→"; 329 color: var(--color-primary); 330 font-size: 0.75rem; 331} 332 333/* ============================================ 334 MAIN CONTENT 335 ============================================ */ 336main { 337 padding: var(--space-md); 338 min-width: 0; /* Prevents grid blowout */ 339} 340 341@media (min-width: 640px) { 342 main { 343 padding: var(--space-lg); 344 } 345} 346 347/* ============================================ 348 TYPOGRAPHY 349 ============================================ */ 350h2 { 351 font-size: var(--text-xl); 352 font-weight: 700; 353 line-height: 1.2; 354 margin: 0 0 var(--space-md); 355 color: var(--color-text); 356} 357 358h2::after { 359 content: ""; 360 display: block; 361 width: 50px; 362 height: 3px; 363 background: var(--color-primary); 364 margin-top: var(--space-xs); 365 border-radius: 2px; 366} 367 368h3 { 369 font-size: var(--text-lg); 370 font-weight: 600; 371 line-height: 1.3; 372 margin: var(--space-md) 0 var(--space-sm); 373 color: var(--color-text); 374} 375 376p { 377 margin-bottom: var(--space-md); 378 max-width: 65ch; 379 overflow-wrap: break-word; 380 hyphens: auto; 381} 382 383a { 384 color: var(--color-primary); 385 text-underline-offset: 0.15em; 386 transition: color var(--transition-fast); 387} 388 389a:hover { 390 color: var(--color-primary-dark); 391} 392 393a:focus-visible { 394 outline: 3px solid var(--color-primary); 395 outline-offset: 2px; 396 border-radius: 2px; 397} 398 399ul, ol { 400 margin-bottom: var(--space-md); 401 padding-left: 1.5rem; 402} 403 404li { 405 margin-bottom: var(--space-xs); 406} 407 408/* ============================================ 409 ARTICLE CARDS — RESPONSIVE GRID 410 ============================================ */ 411.posts-grid { 412 display: grid; 413 gap: var(--space-md); 414 margin-top: var(--space-md); 415} 416 417/* 2 columns on tablet */ 418@media (min-width: 640px) { 419 .posts-grid { 420 grid-template-columns: repeat(2, 1fr); 421 } 422} 423 424/* 1 column on mobile, 2 on tablet, but we already have 2 above */ 425/* Actually let's refine: 1 col mobile, 2 col tablet, back to 1 if sidebar present */ 426@media (min-width: 900px) { 427 .posts-grid { 428 grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); 429 } 430} 431 432article.card { 433 background: var(--color-surface-elevated); 434 border: 1px solid var(--color-border); 435 border-radius: 12px; 436 padding: var(--space-md); 437 display: flex; 438 flex-direction: column; 439 transition: transform var(--transition-base), box-shadow var(--transition-base); 440} 441 442article.card:hover { 443 transform: translateY(-3px); 444 box-shadow: 0 8px 24px var(--shadow-color); 445} 446 447article.card header { 448 position: static; 449 background: none; 450 border: none; 451 backdrop-filter: none; 452 padding: 0; 453 margin-bottom: var(--space-sm); 454} 455 456article.card header p { 457 font-size: var(--text-xs); 458 color: var(--color-text-muted); 459 margin: var(--space-xs) 0 0; 460} 461 462article.card h3 { 463 margin: 0; 464 font-size: var(--text-base); 465} 466 467article.card h3 a { 468 color: var(--color-text); 469 text-decoration: none; 470} 471 472article.card h3 a:hover { 473 color: var(--color-primary); 474} 475 476article.card > p { 477 color: var(--color-text-muted); 478 font-size: var(--text-sm); 479 flex: 1; 480 margin-bottom: var(--space-sm); 481} 482 483.card-footer { 484 display: flex; 485 justify-content: space-between; 486 align-items: center; 487 gap: var(--space-sm); 488 padding-top: var(--space-sm); 489 border-top: 1px solid var(--color-border); 490 margin-top: auto; 491} 492 493.card-footer .read-more { 494 font-size: var(--text-sm); 495 font-weight: 600; 496 text-decoration: none; 497} 498 499.card-footer time { 500 font-size: var(--text-xs); 501 color: var(--color-text-muted); 502} 503 504/* ============================================ 505 ABOUT PAGE 506 ============================================ */ 507.profile-header { 508 display: flex; 509 flex-direction: column; 510 align-items: center; 511 gap: var(--space-md); 512 text-align: center; 513 margin-bottom: var(--space-lg); 514} 515 516@media (min-width: 640px) { 517 .profile-header { 518 flex-direction: row; 519 align-items: flex-start; 520 text-align: left; 521 } 522} 523 524.profile-header img { 525 width: 140px; 526 height: 140px; 527 border-radius: 50%; 528 border: 4px solid var(--color-surface); 529 box-shadow: 0 4px 12px var(--shadow-color); 530 object-fit: cover; 531 flex-shrink: 0; 532} 533 534.profile-header .profile-info { 535 flex: 1; 536 min-width: 0; /* Prevents flex item overflow */ 537} 538 539.profile-header .profile-info h2 { 540 margin-bottom: var(--space-sm); 541} 542 543.profile-header .profile-info h2::after { 544 display: none; 545} 546 547.skills-grid { 548 display: grid; 549 grid-template-columns: 1fr; 550 gap: var(--space-md); 551 margin: var(--space-md) 0; 552} 553 554@media (min-width: 480px) { 555 .skills-grid { 556 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 557 } 558} 559 560.skill-category { 561 background: var(--color-surface); 562 padding: var(--space-md); 563 border-radius: 8px; 564 border-left: 3px solid var(--color-primary); 565} 566 567.skill-category h3 { 568 font-size: var(--text-sm); 569 font-weight: 600; 570 color: var(--color-text-muted); 571 margin-bottom: var(--space-sm); 572 text-transform: uppercase; 573 letter-spacing: 0.05em; 574 margin-top: 0; 575} 576 577.skill-category ul { 578 list-style: none; 579 padding: 0; 580 margin: 0; 581} 582 583.skill-category li { 584 font-size: var(--text-sm); 585 margin-bottom: var(--space-xs); 586} 587 588/* ============================================ 589 CONTACT FORM — ACCESSIBLE 590 ============================================ */ 591.contact-form { 592 margin-top: var(--space-lg); 593} 594 595.form-row { 596 display: flex; 597 flex-direction: column; 598 gap: 0; 599 margin-bottom: 0; 600} 601 602@media (min-width: 640px) { 603 .form-row { 604 flex-direction: row; 605 gap: var(--space-md); 606 } 607} 608 609.form-row > .field { 610 flex: 1; 611 margin-bottom: var(--space-md); 612} 613 614.field { 615 margin-bottom: var(--space-md); 616} 617 618.field label { 619 display: block; 620 font-size: var(--text-sm); 621 font-weight: 600; 622 margin-bottom: var(--space-xs); 623 color: var(--color-text); 624} 625 626.field label .required { 627 color: #dc2626; 628 margin-left: 0.25rem; 629} 630 631.field input[type="text"], 632.field input[type="email"], 633.field input[type="tel"], 634.field select, 635.field textarea { 636 width: 100%; 637 padding: 0.75rem 1rem; 638 border: 2px solid var(--color-border); 639 border-radius: 8px; 640 font-family: inherit; 641 font-size: var(--text-base); 642 color: var(--color-text); 643 background: var(--color-surface-elevated); 644 transition: border-color var(--transition-fast), box-shadow var(--transition-fast); 645 min-height: 44px; /* Minimum touch target */ 646} 647 648.field input:focus, 649.field select:focus, 650.field textarea:focus { 651 outline: none; 652 border-color: var(--color-primary); 653 box-shadow: 0 0 0 3px color-mix(in oklch, var(--color-primary) 20%, transparent); 654 background: var(--color-bg); 655} 656 657/* Invalid state (only after user interaction) */ 658.field input:invalid:not(:placeholder-shown), 659.field textarea:invalid:not(:placeholder-shown) { 660 border-color: #dc2626; 661} 662 663.field:has(input:invalid:not(:placeholder-shown)) label, 664.field:has(textarea:invalid:not(:placeholder-shown)) label { 665 color: #dc2626; 666} 667 668.field .error-message { 669 display: none; 670 font-size: var(--text-xs); 671 color: #dc2626; 672 margin-top: 0.25rem; 673} 674 675.field:has(input:invalid:not(:placeholder-shown)) .error-message, 676.field:has(textarea:invalid:not(:placeholder-shown)) .error-message { 677 display: block; 678} 679 680fieldset { 681 border: 2px solid var(--color-border); 682 border-radius: 8px; 683 padding: var(--space-md); 684 margin-bottom: var(--space-md); 685} 686 687legend { 688 font-weight: 600; 689 padding: 0 var(--space-xs); 690 color: var(--color-text-muted); 691 font-size: var(--text-sm); 692} 693 694.checkbox-row { 695 display: flex; 696 align-items: flex-start; 697 gap: 0.5rem; 698 margin-bottom: var(--space-xs); 699} 700 701.checkbox-row input[type="checkbox"], 702.checkbox-row input[type="radio"] { 703 width: 20px; 704 height: 20px; 705 min-width: 20px; /* Prevents shrinking */ 706 margin-top: 0.15rem; 707 accent-color: var(--color-primary); 708 cursor: pointer; 709} 710 711.checkbox-row label { 712 margin: 0; 713 font-weight: 400; 714 cursor: pointer; 715 line-height: 1.4; 716} 717 718.form-actions { 719 display: flex; 720 flex-direction: column; 721 gap: var(--space-sm); 722} 723 724@media (min-width: 480px) { 725 .form-actions { 726 flex-direction: row; 727 } 728} 729 730button[type="submit"], 731button[type="reset"] { 732 width: 100%; 733 padding: 0.875rem 1.5rem; 734 font-size: var(--text-base); 735 font-weight: 600; 736 border-radius: 8px; 737 cursor: pointer; 738 transition: all var(--transition-fast); 739 min-height: 44px; 740} 741 742@media (min-width: 480px) { 743 button[type="submit"], 744 button[type="reset"] { 745 width: auto; 746 } 747} 748 749button[type="submit"] { 750 background: var(--color-primary); 751 color: #ffffff; 752 border: none; 753} 754 755button[type="submit"]:hover { 756 background: var(--color-primary-dark); 757 transform: translateY(-1px); 758} 759 760button[type="submit"]:active { 761 transform: translateY(0); 762} 763 764button[type="submit"]:focus-visible { 765 outline: 3px solid var(--color-primary); 766 outline-offset: 3px; 767} 768 769button[type="reset"] { 770 background: transparent; 771 color: var(--color-text-muted); 772 border: 2px solid var(--color-border); 773} 774 775button[type="reset"]:hover { 776 background: var(--color-surface); 777 color: var(--color-text); 778 border-color: var(--color-text-muted); 779} 780 781/* ============================================ 782 FOOTER 783 ============================================ */ 784footer { 785 text-align: center; 786 padding: var(--space-lg) var(--space-md); 787 color: var(--color-text-muted); 788 font-size: var(--text-sm); 789 border-top: 1px solid var(--color-border); 790} 791 792footer p { 793 margin: 0 auto var(--space-xs); 794 max-width: none; 795} 796 797footer a { 798 color: var(--color-text-muted); 799 text-decoration: none; 800 margin: 0 var(--space-xs); 801} 802 803footer a:hover { 804 color: var(--color-primary); 805} 806 807footer a:focus-visible { 808 outline: 2px solid var(--color-primary); 809 outline-offset: 2px; 810 border-radius: 2px; 811} 812 813/* ============================================ 814 RESPONSIVE IMAGES UTILITIES 815 ============================================ */ 816img { 817 max-width: 100%; 818 height: auto; 819 display: block; 820} 821 822/* ============================================ 823 TOUCH TARGET SIZES 824 ============================================ */ 825@media (pointer: coarse) { 826 header nav a, 827 .widget a, 828 .card-footer .read-more { 829 min-height: 44px; 830 display: flex; 831 align-items: center; 832 } 833}
Updated contact.html (Accessible Form)
1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Contact | My Personal Blog</title> 7 <link rel="stylesheet" href="styles.css"> 8</head> 9<body> 10 11 <a href="#main-content" class="skip-link">Skip to main content</a> 12 13 <div class="page-layout"> 14 15 <header> 16 <div class="header-inner"> 17 <h1><a href="index.html">My Personal Blog</a></h1> 18 <p>Thoughts on web development, design, and technology</p> 19 <nav aria-label="Main"> 20 <ul> 21 <li><a href="index.html">Home</a></li> 22 <li><a href="about.html">About</a></li> 23 <li><a href="contact.html" aria-current="page">Contact</a></li> 24 </ul> 25 </nav> 26 </div> 27 </header> 28 29 <aside aria-label="Sidebar"> 30 <div class="widget"> 31 <h2>Quick Links</h2> 32 <ul> 33 <li><a href="https://developer.mozilla.org" target="_blank" rel="noopener noreferrer">MDN Docs</a></li> 34 <li><a href="https://caniuse.com" target="_blank" rel="noopener noreferrer">Can I Use</a></li> 35 <li><a href="https://web.dev" target="_blank" rel="noopener noreferrer">Web.dev</a></li> 36 </ul> 37 </div> 38 </aside> 39 40 <main id="main-content"> 41 <section> 42 <h2>Get In Touch</h2> 43 <p>Have a question or collaboration idea? Fill out the form below.</p> 44 45 <form class="contact-form" action="/submit-contact" method="POST" novalidate> 46 47 <div class="form-row"> 48 <div class="field"> 49 <label for="full-name"> 50 Full Name 51 <span class="required" aria-hidden="true">*</span> 52 <span class="sr-only">(required)</span> 53 </label> 54 <input 55 type="text" 56 id="full-name" 57 name="full_name" 58 placeholder="Jane Doe" 59 required 60 minlength="2" 61 autocomplete="name" 62 aria-required="true" 63 aria-describedby="name-error"> 64 <span class="error-message" id="name-error">Please enter your full name (at least 2 characters).</span> 65 </div> 66 67 <div class="field"> 68 <label for="email"> 69 Email Address 70 <span class="required" aria-hidden="true">*</span> 71 <span class="sr-only">(required)</span> 72 </label> 73 <input 74 type="email" 75 id="email" 76 name="email" 77 placeholder="jane@example.com" 78 required 79 autocomplete="email" 80 aria-required="true" 81 aria-describedby="email-error"> 82 <span class="error-message" id="email-error">Please enter a valid email address.</span> 83 </div> 84 </div> 85 86 <div class="field"> 87 <label for="topic">Topic</label> 88 <select id="topic" name="topic"> 89 <option value="" disabled selected>Select a topic</option> 90 <option value="general">General Inquiry</option> 91 <option value="collaboration">Collaboration</option> 92 <option value="feedback">Feedback</option> 93 </select> 94 </div> 95 96 <div class="field"> 97 <label for="message"> 98 Your Message 99 <span class="required" aria-hidden="true">*</span> 100 <span class="sr-only">(required)</span> 101 </label> 102 <textarea 103 id="message" 104 name="message" 105 rows="6" 106 placeholder="Tell me what's on your mind..." 107 required 108 minlength="10" 109 aria-required="true" 110 aria-describedby="message-error"></textarea> 111 <span class="error-message" id="message-error">Please enter a message (at least 10 characters).</span> 112 </div> 113 114 <fieldset> 115 <legend>Preferences</legend> 116 <div class="checkbox-row"> 117 <input type="checkbox" id="newsletter" name="newsletter" value="yes"> 118 <label for="newsletter">Subscribe to my monthly newsletter</label> 119 </div> 120 <div class="checkbox-row"> 121 <input type="checkbox" id="privacy" name="privacy" value="accepted" required aria-required="true"> 122 <label for="privacy"> 123 I agree to the <a href="privacy.html">privacy policy</a> 124 <span class="required" aria-hidden="true">*</span> 125 </label> 126 </div> 127 </fieldset> 128 129 <div class="form-actions"> 130 <button type="submit">Send Message</button> 131 <button type="reset">Clear Form</button> 132 </div> 133 134 </form> 135 </section> 136 </main> 137 138 <footer> 139 <p>© 2026 My Personal Blog. Built with accessible, responsive HTML & CSS.</p> 140 </footer> 141 142 </div> 143 144</body> 145</html>
Part 9: The Testing Checklist
Before you call this module complete, run through this checklist:
Responsive Design
- Test on a real phone (not just DevTools device mode)
- Test on a tablet in both portrait and landscape
- Resize desktop browser from 320px to 2560px — no horizontal scroll at any width
- Check touch targets are at least 44×44px on mobile
- Verify images have
widthandheightattributes (prevents CLS) - Test with slow 3G throttling in DevTools — images should load appropriately sized
Accessibility
- Lighthouse accessibility audit: score 100
- axe DevTools: zero critical or serious issues
- Keyboard-only navigation: can complete every task without a mouse
- Focus indicators visible on all interactive elements
- Skip link works and is the first focusable element
- Screen reader announces page landmarks correctly
- Form labels are associated with inputs (click label, input should focus)
- Color contrast passes WCAG AA for all text
-
prefers-reduced-motionrespected: no animations when enabled -
prefers-color-schemedetected: dark mode matches OS setting
Key Takeaways
| Concept | What It Means |
|---|---|
| Mobile-first | Write base styles for mobile, enhance with min-width queries |
| Content breakpoints | Add breakpoints when content breaks, not at device widths |
prefers-reduced-motion | Respect user choice — disable animations |
prefers-color-scheme | Match OS dark/light mode automatically |
srcset + sizes | Serve appropriately sized images to each device |
<picture> | Art direction: completely different images per breakpoint |
| Touch targets | Minimum 44×44px for all interactive elements |
| Focus indicators | Never remove outlines; style them to match your design |
| ARIA | Use semantic HTML first; add ARIA only when HTML falls short |
| WCAG 2.2 | 4.5:1 contrast for text, keyboard operable, screen reader compatible |
What's Next?
Your blog is now fully responsive and accessible. It works on every device, respects user preferences, and can be navigated by anyone — regardless of ability or technology.
Module 6 covers Performance, Workflow, and Real Projects: CSS architecture with BEM, build tools with Vite, critical CSS for fast loads, Core Web Vitals optimization, and deploying to production. We'll take this blog from "works on localhost" to "live on the internet with a 95+ Lighthouse score."
Save your work. The foundation is solid. Now we ship it.
Quick Reference Cheat Sheet
1/* Mobile-first media query */ 2@media (min-width: 640px) { /* tablet */ } 3@media (min-width: 1024px) { /* desktop */ } 4 5/* Respect user preferences */ 6@media (prefers-reduced-motion: reduce) { /* disable animations */ } 7@media (prefers-color-scheme: dark) { /* dark mode */ } 8 9/* Responsive images */ 10<img srcset="small.jpg 400w, large.jpg 800w" sizes="(max-width: 640px) 100vw, 50vw"> 11 12/* Focus that works */ 13:focus-visible { outline: 3px solid blue; outline-offset: 2px; } 14 15/* Accessible form */ 16<label for="email">Email</label> 17<input id="email" aria-required="true" aria-describedby="error"> 18<span id="error">Please enter a valid email</span> 19 20/* Touch targets */ 21button { min-height: 44px; min-width: 44px; }
Your blog now works for everyone. In Module 6, we optimize it for speed and ship it to production.