Responsive Design
Building websites that adapt to any screen size, from mobile phones to ultra-wide monitors. Master the techniques that make layouts flexible and content accessible everywhere.
Viewport Meta Tag
The single most important line for responsive design. Without it, mobile browsers render the page at a virtual desktop width (~980px) and then scale it down.
<!-- Required in every responsive page's <head> --> <meta name="viewport" content="width=device-width, initial-scale=1.0">
| Attribute | Value | Purpose |
|---|---|---|
width |
device-width |
Match the screen's width in CSS pixels |
initial-scale |
1.0 |
No zoom on page load |
maximum-scale |
1.0 |
Prevent zoom (avoid this for accessibility!) |
user-scalable |
no |
Disable pinch-to-zoom (avoid this!) |
Avoid maximum-scale=1 and user-scalable=no. These break accessibility by preventing users with low vision from zooming in. Many accessibility guidelines (WCAG) require that zoom is not disabled.
Mobile-First vs Desktop-First
Two approaches to writing responsive CSS. Mobile-first is the industry standard and recommended approach.
/* ============================== MOBILE-FIRST (recommended) Base styles = mobile Add complexity as screen grows Uses: min-width ============================== */ /* Base: mobile styles (no media query needed) */ .container { padding: 1rem; font-size: 0.875rem; } @media (min-width: 768px) { .container { padding: 2rem; font-size: 1rem; } } @media (min-width: 1024px) { .container { padding: 3rem; max-width: 1200px; margin: 0 auto; } }
/* ============================== DESKTOP-FIRST (legacy approach) Base styles = desktop Remove complexity as screen shrinks Uses: max-width ============================== */ /* Base: desktop styles */ .container { padding: 3rem; max-width: 1200px; margin: 0 auto; } @media (max-width: 1023px) { .container { padding: 2rem; max-width: none; } } @media (max-width: 767px) { .container { padding: 1rem; } }
1. Performance: Mobile devices get only the CSS they need; desktop enhancements load progressively. 2. Simplicity: It is easier to add layout complexity than to undo it. 3. Future-proof: Most web traffic is mobile, so start there.
Media Queries
The core mechanism for responsive design. Apply CSS rules conditionally based on viewport characteristics.
/* Basic syntax */ @media (min-width: 768px) { /* styles for 768px and wider */ } @media (max-width: 767px) { /* styles for 767px and narrower */ } /* Combining with AND */ @media (min-width: 768px) and (max-width: 1023px) { /* tablet only */ } /* OR (comma-separated) */ @media (max-width: 480px), (orientation: portrait) { /* small screens OR portrait orientation */ } /* NOT */ @media not print { /* everything except print */ } /* Media types */ @media screen { } /* screens only */ @media print { } /* print only */ @media all { } /* all media (default) */
Common Breakpoints
Standard breakpoints used by major CSS frameworks and design systems. These are starting points — adjust based on your content.
| Name | Min-Width | Typical Devices | CSS Variable |
|---|---|---|---|
| xs | 0px |
Small phones | (base styles) |
| sm | 576px |
Large phones (landscape) | --bp-sm: 576px |
| md | 768px |
Tablets | --bp-md: 768px |
| lg | 1024px |
Small laptops, tablets landscape | --bp-lg: 1024px |
| xl | 1280px |
Desktops | --bp-xl: 1280px |
| 2xl | 1536px |
Large desktops, ultra-wide | --bp-2xl: 1536px |
/* Mobile-first breakpoints (most common) */ /* Base: 0 - 575px (mobile) */ @media (min-width: 576px) { /* sm: landscape phones */ } @media (min-width: 768px) { /* md: tablets */ } @media (min-width: 1024px) { /* lg: laptops */ } @media (min-width: 1280px) { /* xl: desktops */ } @media (min-width: 1536px) { /* 2xl: large screens */ }
Rather than targeting specific devices, set breakpoints where your content naturally breaks. Resize the browser and add a breakpoint when the layout starts looking awkward. This future-proofs your design against new device sizes.
Modern Range Syntax
Modern CSS allows comparison operators in media queries, making them more readable and less error-prone.
/* Old syntax */ @media (min-width: 768px) { } @media (max-width: 1023px) { } @media (min-width: 768px) and (max-width: 1023px) { } /* NEW range syntax (supported in all modern browsers) */ @media (width >= 768px) { } @media (width < 1024px) { } @media (768px <= width < 1024px) { } /* Also works with height */ @media (height >= 600px) { } @media (400px <= height <= 800px) { }
The traditional min-width: 768px and max-width: 767px gap can cause issues with fractional pixels. The range syntax (width >= 768px and width < 768px) eliminates this 1px overlap problem entirely. Supported in all browsers since late 2023.
Feature & Preference Queries
Media queries go beyond screen size. Detect user preferences, device capabilities, and more.
prefers-color-scheme
/* Match the user's OS dark/light preference */ @media (prefers-color-scheme: dark) { :root { --bg: #0f172a; --text: #e2e8f0; } } @media (prefers-color-scheme: light) { :root { --bg: #ffffff; --text: #1e293b; } }
prefers-reduced-motion
/* Respect users who prefer reduced motion */ @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } } /* Progressive approach: only animate for users who are OK with it */ @media (prefers-reduced-motion: no-preference) { .element { transition: transform 0.3s ease; } }
orientation
@media (orientation: portrait) { /* taller than wide */ } @media (orientation: landscape) { /* wider than tall */ }
Other Useful Feature Queries
/* High-resolution (retina) displays */ @media (min-resolution: 2dppx) { } /* Hover capability (touchscreens vs mouse) */ @media (hover: hover) { /* device has a mouse/pointer that can hover */ .card:hover { transform: translateY(-4px); } } @media (hover: none) { /* touchscreen — no hover capability */ } /* Pointer precision */ @media (pointer: coarse) { /* touch — make tap targets larger */ button { min-height: 44px; } } /* Prefers contrast */ @media (prefers-contrast: high) { .card { border: 2px solid; } }
Always implement prefers-reduced-motion and prefers-color-scheme in production sites. These are not nice-to-haves — they are essential for users with vestibular disorders and visual sensitivities. Use the progressive enhancement approach: add motion for users who want it.
Responsive Units
Choose the right unit for the right purpose. Each responsive unit has specific strengths.
| Unit | Relative To | Best For |
|---|---|---|
% |
Parent element | Widths, fluid layouts |
vw |
1% of viewport width | Full-width sections, fluid typography |
vh |
1% of viewport height | Full-height sections, hero banners |
dvh |
Dynamic viewport height | Mobile full-height (accounts for address bar) |
svh |
Small viewport height | Smallest possible viewport (address bar visible) |
lvh |
Large viewport height | Largest possible viewport (address bar hidden) |
rem |
Root font size (usually 16px) | Spacing, font sizes, consistent scaling |
em |
Parent element's font size | Component-local spacing, media queries |
/* Responsive units in action */ .hero { height: 100dvh; /* full viewport on mobile (dynamic) */ padding: 5vw; /* scales with viewport width */ } .sidebar { width: 25%; /* 25% of parent */ } .container { max-width: 75rem; /* 1200px at default font size */ padding: 2rem; /* scales if user changes font size */ }
On mobile browsers, 100vh includes the area behind the address bar, causing content to be hidden. Use 100dvh (dynamic viewport height) instead, which adjusts when the address bar shows/hides. Alternatively use 100svh for the smallest viewport size.
Fluid Typography with clamp()
Create text that scales smoothly between a minimum and maximum size based on the viewport width. No breakpoints needed.
/* clamp(minimum, preferred, maximum) */ h1 { font-size: clamp(1.5rem, 4vw, 3rem); /* - Never smaller than 1.5rem (24px) - Scales with viewport at 4vw - Never larger than 3rem (48px) */ } h2 { font-size: clamp(1.25rem, 3vw, 2rem); } p { font-size: clamp(0.875rem, 1.5vw, 1.125rem); } /* Fluid spacing too! */ section { padding: clamp(1rem, 5vw, 4rem); }
This Heading Scales Fluidly
This paragraph text also scales smoothly between 0.8rem and 1.1rem based on the viewport width. Try resizing your browser to see the text size change continuously without any breakpoint jumps.
font-size: clamp(0.8rem, 1.5vw, 1.1rem)
A useful formula: clamp(min, preferred, max) where preferred = min + (max - min) * ((100vw - min-viewport) / (max-viewport - min-viewport)). For practical use, try: clamp(1rem, 0.5rem + 2vw, 2rem). The 0.5rem + 2vw creates a more controlled scaling curve than vw alone.
Responsive Images
Images that adapt to their container and look great on all screen sizes.
/* The essential responsive image rule */ img { max-width: 100%; /* never overflow container */ height: auto; /* maintain aspect ratio */ display: block; /* remove inline spacing */ } /* object-fit: control how images fill their box */ .card-image { width: 100%; height: 200px; object-fit: cover; /* fill box, crop excess */ object-fit: contain; /* fit inside box, may letterbox */ object-fit: fill; /* stretch to fill (distorts) */ object-fit: none; /* natural size, may crop */ object-position: center top; /* position within the box */ } /* aspect-ratio: maintain proportions */ .video-container { width: 100%; aspect-ratio: 16 / 9; /* always 16:9 */ } .avatar { width: 80px; aspect-ratio: 1; /* perfect square */ border-radius: 50%; /* circle */ object-fit: cover; }
object-fit: cover
object-fit: contain
aspect-ratio: 16/9
Set aspect-ratio on images to prevent layout shift while they load. The browser reserves the correct space before the image downloads, resulting in a much smoother loading experience (better CLS score).
Responsive Grid Pattern
The most powerful responsive pattern: a CSS Grid that auto-adjusts columns without any media queries.
/* The auto-responsive grid — no media queries! */ .auto-grid { display: grid; grid-template-columns: repeat( auto-fit, minmax(min(100%, 300px), 1fr) ); gap: 1.5rem; } /* How it works: - auto-fit: create as many columns as fit - min(100%, 300px): prevents overflow on small screens - 1fr: columns grow to fill available space Result: - Wide screen: 4+ columns - Medium screen: 2-3 columns - Small screen: 1 column - ALL with zero media queries! */
Adaptive layouts
Clean code
Ship fast
Improve always
Using minmax(min(100%, 300px), 1fr) instead of just minmax(300px, 1fr) prevents horizontal overflow on screens smaller than 300px. The min() function picks whichever is smaller: 100% of the container or 300px.
Container Queries
The biggest addition to responsive CSS since media queries. Style elements based on their container's size, not the viewport. Perfect for reusable components.
/* Step 1: Define a containment context */ .card-wrapper { container-type: inline-size; /* enable container queries */ container-name: card; /* optional: give it a name */ } /* Shorthand */ .card-wrapper { container: card / inline-size; } /* Step 2: Write container queries */ @container card (min-width: 400px) { .card { display: flex; gap: 1rem; } } @container card (min-width: 600px) { .card { font-size: 1.25rem; } } /* Without name: queries nearest container ancestor */ @container (min-width: 300px) { .card-title { font-size: 1.5rem; } }
Same component, different layout based on container width (not viewport).
Container queries come with new units: cqw (1% of container width), cqh (1% of container height), cqi (1% of inline size), cqb (1% of block size). Use them for truly component-relative sizing: font-size: 5cqi.
Gotchas
Without <meta name="viewport" content="width=device-width, initial-scale=1.0">, no amount of media queries will make your site responsive on mobile. This is the number one cause of "my media queries don't work on phones."
Media queries with px don't respond to user zoom settings. Consider using em for breakpoints: @media (min-width: 48em) instead of 768px. This respects the user's browser font-size setting.
Common causes: elements with fixed widths wider than the viewport, 100vw including scrollbar width, absolute positioning going off-screen. Debug with * { outline: 1px solid red; } to find the overflow culprit. Quick fix: body { overflow-x: hidden; } (but fix the root cause).
100vw includes the width of the vertical scrollbar, which can cause horizontal overflow. Use width: 100% instead of width: 100vw for full-width elements, or use the newer 100dvw unit.
Apple and Google both recommend minimum 44x44px touch targets. Small buttons and links are a major mobile usability issue. Always test with @media (pointer: coarse) to increase target sizes on touch devices.
/* Ensure accessible touch targets */ @media (pointer: coarse) { button, a, input, select { min-height: 44px; min-width: 44px; } }
Pro Tips
Modern responsive design is moving beyond breakpoints. Combine clamp(), min(), max(), CSS Grid with auto-fit/minmax(), and container queries to create layouts that are intrinsically responsive — they adapt without a single media query.
/* A fully responsive layout with ZERO media queries */ .page { max-width: min(90%, 1200px); margin: 0 auto; padding: clamp(1rem, 5vw, 3rem); } .page h1 { font-size: clamp(1.5rem, 4vw, 3rem); } .card-grid { display: grid; grid-template-columns: repeat( auto-fit, minmax(min(100%, 300px), 1fr) ); gap: clamp(0.75rem, 2vw, 1.5rem); }
Browser DevTools device emulation is useful but imperfect. Real devices have different rendering engines, touch behaviors, and viewport quirks (especially iOS Safari). Use services like BrowserStack for comprehensive testing, and always test on at least one real phone.
Instead of margin-left, use margin-inline-start. Instead of width, use inline-size. Logical properties automatically adapt to different writing modes (RTL languages, vertical text), making your responsive design truly global.
/* Physical (breaks in RTL) */ .sidebar { margin-left: 2rem; padding-right: 1rem; border-left: 3px solid blue; } /* Logical (works in all directions) */ .sidebar { margin-inline-start: 2rem; padding-inline-end: 1rem; border-inline-start: 3px solid blue; }
Responsive images are the biggest performance opportunity. Use the HTML <picture> element or srcset attribute to serve appropriately sized images. A 2000px hero image on a 375px phone wastes enormous bandwidth. Use loading="lazy" for below-the-fold images.
<!-- Responsive images with srcset --> <img src="image-800.jpg" srcset="image-400.jpg 400w, image-800.jpg 800w, image-1200.jpg 1200w" sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 33vw" alt="Description" loading="lazy" >