Responsive Design
Responsive Web Design (RWD) is an approach that ensures web pages render well across all devices — from small phones to large desktop monitors. The core principles, defined by Ethan Marcotte in 2010, are three pillars: fluid grids, flexible images, and media queries.
Modern responsive design has evolved to include container queries, viewport units, clamp(), responsive typography, and responsive images with srcset. The goal is one codebase that adapts to any context rather than building separate mobile and desktop sites.
| 1 | /* Fluid grid foundation */ |
| 2 | .container { |
| 3 | width: 100%; |
| 4 | max-width: 1200px; |
| 5 | margin-inline: auto; |
| 6 | padding-inline: 1rem; |
| 7 | } |
| 8 | |
| 9 | /* Flexible images */ |
| 10 | img, video, iframe { |
| 11 | max-width: 100%; |
| 12 | height: auto; |
| 13 | } |
| 14 | |
| 15 | /* Media query for larger screens */ |
| 16 | @media (min-width: 768px) { |
| 17 | .container { |
| 18 | padding-inline: 2rem; |
| 19 | } |
| 20 | } |
The viewport meta tag controls how a webpage is displayed on mobile devices. Without it, mobile browsers render pages at a desktop width (typically 980px) and then zoom out — resulting in tiny, unreadable text.
| 1 | <!-- Standard viewport configuration (recommended) --> |
| 2 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 3 | |
| 4 | <!-- With user-scalable=no (not recommended for accessibility) --> |
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> |
| 6 | |
| 7 | <!-- With maximum-scale restriction --> |
| 8 | <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"> |
| 9 | |
| 10 | <!-- Minimum viable viewport tag --> |
| Attribute | Values | Description |
|---|---|---|
| width | device-width | <number> | Sets the viewport width. device-width matches the device screen width. |
| initial-scale | 0.1 — 10 | Initial zoom level. 1.0 = no zoom. |
| minimum-scale | 0.1 — 10 | Minimum allowed zoom level. |
| maximum-scale | 0.1 — 10 | Maximum allowed zoom level. |
| user-scalable | yes | no | Allow pinch-to-zoom. Setting no is an accessibility violation. |
warning
Media queries apply CSS conditionally based on device characteristics, viewport size, or user preferences. They are the backbone of responsive design, enabling different layouts, typography, and interactions across device contexts.
Media Query Syntax
| 1 | /* @media syntax: @media <media-type> (<feature>: <value>) { ... } */ |
| 2 | |
| 3 | /* Width-based queries */ |
| 4 | @media (min-width: 768px) { /* tablet and up */ } |
| 5 | @media (max-width: 767px) { /* mobile only */ } |
| 6 | @media (width: 1024px) { /* exact width */ } |
| 7 | |
| 8 | /* Range syntax (modern browsers) */ |
| 9 | @media (768px <= width <= 1024px) { /* between 768px and 1024px */ } |
| 10 | @media (width >= 768px) { /* same as min-width: 768px */ } |
| 11 | @media (width <= 1024px) { /* same as max-width: 1024px */ } |
| 12 | |
| 13 | /* Orientation */ |
| 14 | @media (orientation: portrait) { /* height > width */ } |
| 15 | @media (orientation: landscape) { /* width > height */ } |
| 16 | |
| 17 | /* Resolution (for Retina / high-DPI) */ |
| 18 | @media (min-resolution: 2dppx) { /* 2x screens */ } |
| 19 | |
| 20 | /* Hover capability */ |
| 21 | @media (hover: hover) { /* device has a hover-capable pointer */ } |
| 22 | @media (hover: none) { /* touch device — no hover */ } |
| 23 | |
| 24 | /* Pointer precision */ |
| 25 | @media (pointer: fine) { /* mouse, stylus */ } |
| 26 | @media (pointer: coarse) { /* finger */ } |
| 27 | |
| 28 | /* User preference queries */ |
| 29 | @media (prefers-color-scheme: dark) { /* dark mode */ } |
| 30 | @media (prefers-color-scheme: light) { /* light mode */ } |
| 31 | @media (prefers-reduced-motion: reduce) { /* reduced motion */ } |
| 32 | @media (prefers-contrast: more) { /* high contrast */ } |
| 33 | @media (prefers-reduced-data: reduce) { /* data saver */ } |
| 34 | |
| 35 | /* Combining conditions */ |
| 36 | @media (min-width: 768px) and (hover: hover) { |
| 37 | /* tablet/desktop with mouse */ |
| 38 | } |
| 39 | |
| 40 | @media not (pointer: fine) { |
| 41 | /* touch or coarse pointer devices */ |
| 42 | } |
| 43 | |
| 44 | /* Comma-separated = OR */ |
| 45 | @media (max-width: 480px), (orientation: landscape) { |
| 46 | /* either condition */ |
| 47 | } |
Fluid layouts use relative units instead of fixed pixel values to adapt to any viewport size. The combination of percentage-based widths, viewport units, max-width, and CSS math functions creates layouts that stretch and compress smoothly.
| 1 | /* Fluid container */ |
| 2 | .container { |
| 3 | width: min(100% - 2rem, 1200px); |
| 4 | margin-inline: auto; |
| 5 | } |
| 6 | |
| 7 | /* Fluid columns */ |
| 8 | .sidebar { |
| 9 | width: 30%; /* always 30% of parent */ |
| 10 | max-width: 300px; /* but never wider than 300px */ |
| 11 | min-width: 200px; /* never narrower than 200px */ |
| 12 | } |
| 13 | |
| 14 | .main { |
| 15 | width: 70%; |
| 16 | min-width: 0; /* allow shrinking below content size */ |
| 17 | } |
| 18 | |
| 19 | /* Using calc() for fluid sizing */ |
| 20 | .half-gap { |
| 21 | width: calc(50% - 12px); /* half minus a gap */ |
| 22 | } |
| 23 | |
| 24 | /* Using clamp() for fluid sizing */ |
| 25 | .responsive-box { |
| 26 | width: clamp(280px, 50%, 600px); |
| 27 | /* minimum 280px, preferred 50%, maximum 600px */ |
| 28 | } |
| 29 | |
| 30 | /* Full-bleed sections */ |
| 31 | .full-bleed { |
| 32 | width: 100vw; |
| 33 | margin-inline: calc(-50vw + 50%); |
| 34 | } |
| 35 | |
| 36 | /* Aspect ratio boxes */ |
| 37 | .square { aspect-ratio: 1 / 1; } |
| 38 | .video { aspect-ratio: 16 / 9; } |
| 39 | .card { aspect-ratio: 3 / 4; } |
Modern CSS layout methods (Flexbox and Grid) have built-in responsive capabilities that reduce or eliminate the need for media queries in common patterns.
Auto-fill vs Auto-fit
| Keyword | Behavior | Result |
|---|---|---|
| auto-fill | Creates as many tracks as fit, even if empty | Track space is preserved (may leave empty gaps) |
| auto-fit | Creates tracks and collapses empty ones | Items stretch to fill the container |
| 1 | /* Responsive Grid — no media queries needed */ |
| 2 | .card-grid { |
| 3 | display: grid; |
| 4 | grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); |
| 5 | gap: 1.5rem; |
| 6 | /* Automatically adjusts columns based on available width */ |
| 7 | } |
| 8 | |
| 9 | /* Flexbox wrapping row */ |
| 10 | .flex-row { |
| 11 | display: flex; |
| 12 | flex-wrap: wrap; |
| 13 | gap: 1rem; |
| 14 | } |
| 15 | |
| 16 | .flex-row > * { |
| 17 | flex: 1 1 250px; /* grow, shrink, basis */ |
| 18 | /* Minimum width of 250px, wraps when narrower */ |
| 19 | } |
| 20 | |
| 21 | /* Flex shorthand for responsive items */ |
| 22 | .sidebar { flex: 0 1 280px; } /* fixed-ish sidebar */ |
| 23 | .content { flex: 1 1 0%; } /* fills remaining space */ |
| 24 | .nav-item { flex: 0 1 auto; } /* sized by content */ |
| 25 | |
| 26 | /* minmax() for responsive grids */ |
| 27 | .grid { |
| 28 | grid-template-columns: |
| 29 | repeat(auto-fit, minmax(min(280px, 100%), 1fr)); |
| 30 | /* Safer — never overflows on very small screens */ |
| 31 | } |
Responsive images deliver the right image size for the user's device and viewport, saving bandwidth and improving load times. The srcset and sizes attributes, along with the <picture> element, give you fine control over image delivery.
srcset & sizes
| 1 | <!-- srcset: list of image files with their widths --> |
| 2 | <img |
| 3 | src="photo-800.jpg" |
| 4 | srcset=" |
| 5 | photo-400.jpg 400w, |
| 6 | photo-800.jpg 800w, |
| 7 | photo-1200.jpg 1200w" |
| 8 | sizes=" |
| 9 | (max-width: 600px) 100vw, |
| 10 | (max-width: 1200px) 50vw, |
| 11 | 800px" |
| 12 | alt="Responsive image example" |
| 13 | > |
| 14 | |
| 15 | <!-- Density descriptors (2x, 3x for Retina) --> |
| 16 | <img |
| 17 | src="photo.jpg" |
| 18 | srcset=" |
| 19 | photo.jpg 1x, |
| 20 | photo-2x.jpg 2x, |
| 21 | photo-3x.jpg 3x" |
| 22 | alt="Retina responsive image" |
| 23 | > |
The picture element
| 1 | <!-- <picture> for art direction — different crops per breakpoint --> |
| 2 | <picture> |
| 3 | <source media="(min-width: 1200px)" srcset="hero-desktop.jpg"> |
| 4 | <source media="(min-width: 768px)" srcset="hero-tablet.jpg"> |
| 5 | <source media="(min-width: 480px)" srcset="hero-phone.jpg"> |
| 6 | <img src="hero-fallback.jpg" alt="Hero image"> |
| 7 | </picture> |
| 8 | |
| 9 | <!-- WebP with fallback --> |
| 10 | <picture> |
| 11 | <source type="image/webp" srcset="image.webp"> |
| 12 | <source type="image/avif" srcset="image.avif"> |
| 13 | <img src="image.jpg" alt="Image with modern format fallback"> |
| 14 | </picture> |
CSS Image Techniques
| 1 | /* Aspect ratio containers */ |
| 2 | .image-wrapper { |
| 3 | aspect-ratio: 16 / 9; |
| 4 | overflow: hidden; |
| 5 | } |
| 6 | |
| 7 | .image-wrapper img { |
| 8 | width: 100%; |
| 9 | height: 100%; |
| 10 | object-fit: cover; /* crop to fill */ |
| 11 | object-position: center; /* center crop */ |
| 12 | } |
| 13 | |
| 14 | /* Object-fit options */ |
| 15 | .contain { object-fit: contain; } /* fit within, may have letterbox */ |
| 16 | .cover { object-fit: cover; } /* fill container, may crop */ |
| 17 | .fill { object-fit: fill; } /* stretch to fill */ |
| 18 | .none { object-fit: none; } /* original size */ |
| 19 | .scale-down { object-fit: scale-down; } /* smaller of contain or none */ |
| 20 | |
| 21 | /* image-set() for resolution-based backgrounds */ |
| 22 | .hero { |
| 23 | background-image: image-set( |
| 24 | url("hero-1x.jpg") 1x, |
| 25 | url("hero-2x.jpg") 2x, |
| 26 | url("hero-3x.jpg") 3x |
| 27 | ); |
| 28 | background-size: cover; |
| 29 | } |
pro tip
Typography must adapt to screen size to maintain readability. Small screens need smaller text (less viewing distance) while preserving a comfortable line length. Fluid typography using clamp() eliminates the need for breakpoint-specific font sizes.
| 1 | /* Fluid type scale using clamp() */ |
| 2 | :root { |
| 3 | --text-sm: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem); |
| 4 | --text-base: clamp(0.875rem, 0.8rem + 0.5vw, 1rem); |
| 5 | --text-lg: clamp(1rem, 0.9rem + 0.8vw, 1.25rem); |
| 6 | --text-xl: clamp(1.25rem, 1.1rem + 1.2vw, 1.75rem); |
| 7 | --text-2xl: clamp(1.5rem, 1.2rem + 2vw, 2.5rem); |
| 8 | --text-3xl: clamp(2rem, 1.5rem + 3vw, 4rem); |
| 9 | } |
| 10 | |
| 11 | body { |
| 12 | font-size: var(--text-base); |
| 13 | line-height: 1.6; |
| 14 | } |
| 15 | |
| 16 | h1 { font-size: var(--text-3xl); line-height: 1.1; } |
| 17 | h2 { font-size: var(--text-2xl); line-height: 1.15; } |
| 18 | h3 { font-size: var(--text-xl); line-height: 1.2; } |
| 19 | |
| 20 | /* Line length control */ |
| 21 | p { |
| 22 | max-width: 65ch; /* optimal line length */ |
| 23 | } |
| 24 | |
| 25 | /* Adjust line-height for different viewports */ |
| 26 | @media (max-width: 480px) { |
| 27 | p { |
| 28 | line-height: 1.7; /* more spacing on small screens */ |
| 29 | } |
| 30 | } |
| 31 | |
| 32 | /* Modular scale for headings */ |
| 33 | :root { |
| 34 | --modular-ratio: 1.25; /* major third */ |
| 35 | } |
| 36 | |
| 37 | h1 { font-size: clamp(2rem, 1.5rem + 2vw, calc(1rem * var(--modular-ratio) * 4)); } |
| 38 | h2 { font-size: clamp(1.5rem, 1.2rem + 1.5vw, calc(1rem * var(--modular-ratio) * 3)); } |
| 39 | h3 { font-size: clamp(1.25rem, 1rem + 1vw, calc(1rem * var(--modular-ratio) * 2)); } |
Breakpoints are viewport widths where the layout changes to accommodate the screen size. Choose breakpoints based on your content, not specific devices. Mobile-first (min-width) is the recommended approach.
Common Breakpoint Ranges
| Name | Width | Typical Devices |
|---|---|---|
| Mobile | 320px — 480px | Small phones |
| Mobile+ | 481px — 767px | Large phones, small tablets |
| Tablet | 768px — 1023px | iPads, Android tablets (portrait) |
| Desktop | 1024px — 1439px | Laptops, desktops |
| Wide | 1440px+ | Large monitors, ultra-wide |
Mobile-First vs Desktop-First
| 1 | /* Mobile-first approach — recommended */ |
| 2 | /* Base styles = mobile */ |
| 3 | .layout { |
| 4 | display: flex; |
| 5 | flex-direction: column; |
| 6 | gap: 1rem; |
| 7 | } |
| 8 | |
| 9 | /* Tablet */ |
| 10 | @media (min-width: 768px) { |
| 11 | .layout { |
| 12 | flex-direction: row; |
| 13 | flex-wrap: wrap; |
| 14 | } |
| 15 | .sidebar { flex: 0 0 250px; } |
| 16 | .main { flex: 1 1 0%; } |
| 17 | } |
| 18 | |
| 19 | /* Desktop */ |
| 20 | @media (min-width: 1024px) { |
| 21 | .layout { |
| 22 | max-width: 1200px; |
| 23 | margin-inline: auto; |
| 24 | } |
| 25 | } |
| 26 | |
| 27 | /* Desktop-first approach (not recommended) */ |
| 28 | /* Base styles = desktop */ |
| 29 | .layout-desktop-first { |
| 30 | display: grid; |
| 31 | grid-template-columns: 250px 1fr; |
| 32 | gap: 2rem; |
| 33 | } |
| 34 | |
| 35 | /* Tablet — override desktop */ |
| 36 | @media (max-width: 1023px) { |
| 37 | .layout-desktop-first { |
| 38 | grid-template-columns: 200px 1fr; |
| 39 | gap: 1.5rem; |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | /* Mobile — full override */ |
| 44 | @media (max-width: 767px) { |
| 45 | .layout-desktop-first { |
| 46 | grid-template-columns: 1fr; |
| 47 | } |
| 48 | } |
Content-Driven Breakpoints
Rather than targeting specific devices, add a breakpoint when the content demands it — when a line becomes too long, when a sidebar gets too narrow, or when a card row wraps awkwardly.
| 1 | /* Content-driven approach — breakpoints at content limits */ |
| 2 | .card-grid { |
| 3 | display: grid; |
| 4 | grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); |
| 5 | gap: 1.5rem; |
| 6 | /* Already responsive — no media query needed */ |
| 7 | } |
| 8 | |
| 9 | /* Add a media query only when content breaks */ |
| 10 | @media (min-width: 1000px) { |
| 11 | /* At this width, the sidebar has enough space */ |
| 12 | .sidebar { |
| 13 | display: block; /* show previously hidden sidebar */ |
| 14 | } |
| 15 | } |
| 16 | |
| 17 | @media (min-width: 72rem) { |
| 18 | /* At this width, the hero text fits on one line */ |
| 19 | .hero-title { |
| 20 | white-space: nowrap; |
| 21 | } |
| 22 | } |
best practice
Container queries allow you to style elements based on their parent container's size rather than the viewport. This is a paradigm shift — components can adapt to their available space regardless of the overall viewport size. Perfect for reusable components that appear in different contexts.
| 1 | /* Step 1: Define a containment context */ |
| 2 | .card-container { |
| 3 | container-type: inline-size; |
| 4 | container-name: card; |
| 5 | } |
| 6 | |
| 7 | /* Step 2: Query the container */ |
| 8 | @container card (min-width: 400px) { |
| 9 | .card { |
| 10 | display: grid; |
| 11 | grid-template-columns: 200px 1fr; |
| 12 | gap: 1rem; |
| 13 | } |
| 14 | |
| 15 | .card-image { |
| 16 | aspect-ratio: 1; |
| 17 | } |
| 18 | |
| 19 | .card-title { |
| 20 | font-size: 1.5rem; |
| 21 | } |
| 22 | } |
| 23 | |
| 24 | @container card (max-width: 399px) { |
| 25 | .card { |
| 26 | display: flex; |
| 27 | flex-direction: column; |
| 28 | } |
| 29 | |
| 30 | .card-title { |
| 31 | font-size: 1.125rem; |
| 32 | } |
| 33 | |
| 34 | .card-cta { |
| 35 | width: 100%; |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | /* Container query shorthand */ |
| 40 | @container (min-width: 500px) { |
| 41 | /* affects nearest container */ |
| 42 | } |
| 43 | |
| 44 | /* container-type values */ |
| 45 | .wrapper { |
| 46 | container-type: normal; /* no containment */ |
| 47 | container-type: inline-size; /* query width (most common) */ |
| 48 | container-type: size; /* query width AND height */ |
| 49 | } |
| 50 | |
| 51 | /* container shorthand */ |
| 52 | .component { |
| 53 | container: card / inline-size; |
| 54 | } |
Container Queries vs Media Queries
| Aspect | Media Queries | Container Queries |
|---|---|---|
| Reference | Viewport / device | Parent container |
| Component reuse | Component may break in different contexts | Component adapts to ANY context |
| Scope | Global — affects entire page | Scoped — only affects children of the container |
| Use case | Page layout, device-specific adjustments | Self-contained components, sidebars, widgets |
| Browser support | ✓ Universal | ✓ Chrome 105+, Safari 16+, Firefox 110+ |
Testing responsive designs requires multiple approaches: browser DevTools for quick iteration, real device testing for accuracy, and automated tools for coverage. No emulator is a perfect substitute for testing on actual hardware.
Testing Tools
| Tool | Best For | Notes |
|---|---|---|
| Chrome DevTools | Rapid prototyping, device emulation | Device toolbar, responsive mode, throttling |
| Safari Web Inspector | iOS testing, WebKit quirks | Responsive design mode, iCloud device sync |
| Firefox Responsive Mode | CSS Grid/Flexbox debugging | Best grid inspector, network throttling |
| BrowserStack | Cross-browser, real devices | Cloud-based real device testing, 3000+ devices |
| Lighthouse | Performance, best practices audit | Mobile/desktop audits, accessibility checks |
| Playwright/Cypress | Automated responsive testing | Programmatic viewport testing, visual regression |
Testing Checklist
info
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.