Responsive Design
clamp(), handle responsive images, and work with viewport units. After this, none of your layouts will ever break on mobile again.
Mobile-First Thinking
The mobile-first approach means you write your base CSS for small screens first, then use media queries to add styles for larger screens. This is the opposite of the old "desktop-first" approach — and it's far better because mobile is now the majority of web traffic.
The mental model is simple: write styles for the smallest screen first. Everything you put in your base CSS applies to phones. Then use min-width media queries to override or add styles as the screen grows.
/* ✅ Mobile-first (recommended) */ .card { display: block; /* single column on mobile */ padding: 1rem; } @media (min-width: 768px) { .card { display: flex; } /* side-by-side on tablet+ */ } /* ❌ Desktop-first (avoid) */ .card { display: flex; /* starts with flex */ } @media (max-width: 767px) { .card { display: block; } /* hack it back for mobile */ }
Media Queries
A media query applies CSS only when certain conditions are true — like screen width, orientation, or display type. The syntax is @media (condition) { ... }. You'll mostly use min-width breakpoints.
/* Base styles — mobile (no query needed) */ .container { padding: 1rem; } /* Small tablet — 480px and up */ @media (min-width: 480px) { .container { padding: 1.5rem; } } /* Tablet — 768px and up */ @media (min-width: 768px) { .container { padding: 2rem; } .grid { grid-template-columns: repeat(2, 1fr); } } /* Desktop — 1024px and up */ @media (min-width: 1024px) { .grid { grid-template-columns: repeat(3, 1fr); } } /* Wide desktop — 1280px and up */ @media (min-width: 1280px) { .container { max-width: 1200px; margin: 0 auto; } }
1 column
1–2 cols
2 columns
3 columns
max-width
/* Orientation */ @media (orientation: landscape) { ... } @media (orientation: portrait) { ... } /* Combining with AND */ @media (min-width: 768px) and (max-width: 1023px) { /* tablet ONLY */ } /* Hover capability (touch vs mouse) */ @media (hover: hover) { .btn:hover { background: blue; } /* only on pointer devices */ } /* Dark mode preference */ @media (prefers-color-scheme: dark) { body { background: #000; } } /* Reduced motion (accessibility!) */ @media (prefers-reduced-motion: reduce) { * { animation: none !important; } }
Fluid Typography with clamp()
Instead of setting a font size at every breakpoint, clamp() lets you define a minimum, preferred, and maximum value in one declaration. The browser smoothly scales the value between those bounds based on viewport width.
/* clamp(minimum, preferred, maximum) */ h1 { font-size: clamp(1.8rem, 5vw, 4rem); /* never smaller than 1.8rem */ /* grows with 5% of viewport width */ /* never larger than 4rem */ } h2 { font-size: clamp(1.4rem, 3.5vw, 2.5rem); } p { font-size: clamp(0.9rem, 2vw, 1.1rem); } /* Works for padding and gaps too! */ .section { padding: clamp(2rem, 8vw, 6rem); } .grid { gap: clamp(1rem, 3vw, 2rem); }
preferred = (max - min) / (max_vw - min_vw) * 100vw + (min - (max - min) / (max_vw - min_vw) * min_vw). Or just use clamp.font-size.app — plug in your min/max font sizes and viewport widths, get the formula instantly.Responsive Images & Viewport Units
Images that overflow their container are the most common responsive bug. The fix is simple. Viewport units (vw, vh, vmin, vmax) let you size elements relative to the screen — great for hero sections and full-screen layouts.
/* The golden rule — always add this */ img { max-width: 100%; /* never overflow container */ height: auto; /* maintain aspect ratio */ display: block; /* remove inline gap */ } /* Responsive hero image with object-fit */ .hero-img { width: 100%; height: clamp(200px, 40vw, 500px); object-fit: cover; /* fill container, crop if needed */ object-position: center top; /* keep the focus point */ } /* srcset for serving different sizes */
<!-- Browser picks the right size automatically --> <img src="hero-800.jpg" srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w" sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 800px" alt="Hero image" loading="lazy" >
/* Viewport units */ .hero { height: 100vh; } /* 100% of viewport height */ .wide { width: 80vw; } /* 80% of viewport width */ .icon { font-size: 5vmin; } /* 5% of smaller dimension */ /* Modern: dvh fixes the mobile browser bar bug */ .hero { height: 100dvh; } /* dynamic viewport height */ /* Centering a container with max-width */ .container { width: min(90%, 1200px); /* 90vw but never over 1200px */ margin: 0 auto; }
| Unit | Meaning | Best for |
|---|---|---|
| vw | 1% of viewport width | Fluid widths, font sizes |
| vh | 1% of viewport height | Full-screen sections |
| dvh | Dynamic viewport height | Mobile (accounts for address bar) |
| vmin | 1% of smaller dimension | Square elements, icons |
| vmax | 1% of larger dimension | Full-bleed backgrounds |
| min() | Smaller of two values | Max-width containers |
| max() | Larger of two values | Minimum safe padding |
item
item
item
item
font-size: clamp(1rem, 3vw, 2rem) do?vh and dvh?width: min(90%, 1200px) do?Create a page with a blog listing. On mobile it should be a single stacked column. On tablet (768px+) show 2 columns. On desktop (1024px+) show a 3-column article grid with a fixed sidebar. Use
clamp() for the heading font size and min(90%, 1100px) for the container. Make sure images don't overflow with max-width: 100%. Bonus: add a responsive navigation that collapses to a hamburger on mobile.
💡 Show hints
- Start with the mobile layout — single column, no grid
- Add
@media (min-width: 768px)for 2-column grid - Add
@media (min-width: 1024px)for 3-col + sidebar using CSS Grid with named areas - Use
clamp(1.5rem, 4vw, 2.5rem)for article titles - For the hamburger, toggle a class on the nav with JavaScript
- Use
position: sticky; top: 0;for a sticky nav