Responsive Web Design: Mobile-First Approach
You build a page on a 27-inch monitor, it looks great, and then someone opens it on their phone. The sidebar squeezes the content into a 120px column, the hero image is a 2MB file scaled down to thumbnail size, and the stylesheet has three max-width blocks fighting each other to undo the desktop layout. Most sites get more than half their traffic from phones, so this isn't an edge case.
Mobile-first flips the order: write the small-screen styles first, then add layout for bigger screens. This post covers why that order matters, how to write min-width media queries, responsive images, fluid grids and type, and the mistakes I run into most often.
Your styles outside any media query are the mobile styles. Everything inside a min-width query adds layout for wider screens. Phones get the simple version, and nothing has to be undone.
Why Mobile-First Matters
This is as much about performance as design. Desktop-first means phones download the full layout, wide images and all, and then CSS hides or overrides most of it. Mobile-first starts from the lean version and only adds what each screen size actually needs.
Desktop-first loads everything then hides; Mobile-first starts lean and adds progressively
Key Benefits of Mobile-First
Performance Benefits
- Fewer overrides: phones apply the base rules and skip the media query blocks they don't match
- Lighter pages on phones: paired with responsive images, small screens stop downloading desktop-sized assets
- Easier Core Web Vitals: smaller images and simpler layouts help LCP and CLS, and less work on the main thread helps INP
- Progressive enhancement: complexity only shows up where there's room for it
Understanding Breakpoints
A breakpoint is just the width where your layout changes. There's no official list, but most teams end up close to Tailwind's defaults, so they're a reasonable starting point:
Common Breakpoint Ranges
| Breakpoint | Min Width | Target Devices | Tailwind Class |
|---|---|---|---|
| sm | 640px | Large phones, small tablets | sm: |
| md | 768px | Tablets (portrait) | md: |
| lg | 1024px | Tablets held sideways, small laptops | lg: |
| xl | 1280px | Laptops, desktops | xl: |
| 2xl | 1536px | Large desktops, 4K monitors | 2xl: |
You don't need a breakpoint for every device. Drag the browser window wider and add one where your content actually breaks: lines of text get too long to read, or cards look cramped. The content should decide the breakpoints, not a list of device specs.
Mobile-First Media Queries
In practice, mobile-first CSS mostly means writing min-width queries instead of max-width ones. The base styles apply everywhere, and each query adds to them as the screen gets wider.
If your queries fire in DevTools but not on a real phone, check for <meta name="viewport" content="width=device-width, initial-scale=1"> in the <head>. Without it, mobile browsers render the page at a virtual width of around 980px and zoom out, so your min-width: 768px styles apply on a phone and the base mobile styles never show up.
Desktop-First vs Mobile-First Syntax
Desktop-First
Uses max-width: starts large, scales down
/* Base: Desktop styles */
.container {
width: 1200px;
display: flex;
}
/* Override for tablet */
@media (max-width: 1024px) {
.container {
width: 100%;
}
}
/* Override for mobile */
@media (max-width: 768px) {
.container {
display: block;
}
}
Mobile-First
Uses min-width: starts small, scales up
/* Base: Mobile styles */
.container {
width: 100%;
display: block;
}
/* Enhance for tablet */
@media (min-width: 768px) {
.container {
display: flex;
}
}
/* Enhance for desktop */
@media (min-width: 1024px) {
.container {
width: 1200px;
}
}
Complete Mobile-First Layout Example
/* ========================================
BASE STYLES (Mobile First - No Media Query)
======================================== */
/* Reset & Base Typography */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, sans-serif;
font-size: 16px;
line-height: 1.6;
color: #1a1a1a;
}
/* Container - Full width with padding on mobile */
.container {
width: 100%;
padding: 0 16px;
}
/* Navigation - Stacked on mobile */
.nav {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
}
.nav-logo {
font-size: 1.5rem;
font-weight: 700;
}
.nav-links {
display: none; /* Hidden by default, toggle with JS */
}
.nav-links.active {
display: flex;
flex-direction: column;
gap: 8px;
}
/* Hero - Single column */
.hero {
padding: 48px 16px;
text-align: center;
}
.hero h1 {
font-size: 2rem;
margin-bottom: 16px;
}
.hero p {
font-size: 1rem;
color: #666;
}
/* Cards Grid - Single column stack */
.cards {
display: grid;
grid-template-columns: 1fr;
gap: 24px;
padding: 24px 16px;
}
.card {
padding: 24px;
border-radius: 12px;
background: #f5f5f5;
}
/* ========================================
TABLET STYLES (min-width: 768px)
======================================== */
@media (min-width: 768px) {
.container {
padding: 0 32px;
}
/* Navigation - Horizontal */
.nav {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.nav-links {
display: flex;
flex-direction: row;
gap: 24px;
}
.nav-toggle {
display: none; /* Hide hamburger */
}
/* Hero - Larger text */
.hero {
padding: 80px 32px;
}
.hero h1 {
font-size: 2.5rem;
}
/* Cards - 2 columns */
.cards {
grid-template-columns: repeat(2, 1fr);
padding: 32px;
}
}
/* ========================================
DESKTOP STYLES (min-width: 1024px)
======================================== */
@media (min-width: 1024px) {
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 48px;
}
/* Hero - Side by side layout */
.hero {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 48px;
text-align: left;
padding: 120px 48px;
align-items: center;
}
.hero h1 {
font-size: 3.5rem;
}
.hero p {
font-size: 1.25rem;
}
/* Cards - 3 columns */
.cards {
grid-template-columns: repeat(3, 1fr);
gap: 32px;
}
}
/* ========================================
LARGE DESKTOP (min-width: 1536px)
======================================== */
@media (min-width: 1536px) {
.container {
max-width: 1400px;
}
.cards {
grid-template-columns: repeat(4, 1fr);
}
}
Responsive Images
Images are usually the heaviest thing on the page, and CSS can't fix that. A 1600px photo shrunk to fit a phone still downloads at full size. You have to tell the browser which files exist.
The srcset Attribute
srcset lists the available widths, and sizes tells the browser how wide the image will render. The browser then picks a file based on that and the screen's pixel density:
<img
src="image-400.jpg"
srcset="
image-400.jpg 400w,
image-800.jpg 800w,
image-1200.jpg 1200w,
image-1600.jpg 1600w
"
sizes="
(max-width: 640px) 100vw,
(max-width: 1024px) 50vw,
33vw
"
alt="Responsive image example"
loading="lazy"
/>
How sizes Works
(max-width: 640px) 100vw: image takes the full viewport width(max-width: 1024px) 50vw: image takes half the viewport33vw: the default, image takes one third of the viewportOne gotcha: if you leave out sizes, the browser assumes 100vw. Your image might sit in a 300px sidebar, but a high-density laptop screen will still download the 1600px file. If the network tab shows bigger images than you expected, check sizes first.
The <picture> Element
Use <picture> when you need different images (not just sizes) for different breakpoints, such as a tighter crop on phones or a newer format with a fallback. The browser uses the first <source> that matches, so order matters:
<picture>
<!-- WebP for modern browsers -->
<source
type="image/webp"
srcset="hero-mobile.webp 640w, hero-desktop.webp 1200w"
sizes="100vw"
/>
<!-- Different crop for mobile -->
<source
media="(max-width: 640px)"
srcset="hero-mobile-crop.jpg"
/>
<!-- Desktop version -->
<source
media="(min-width: 641px)"
srcset="hero-desktop.jpg"
/>
<!-- Fallback -->
<img src="hero-fallback.jpg" alt="Hero image" loading="lazy" />
</picture>
Fluid Layouts with CSS Grid & Flexbox
Grid and Flexbox can handle a lot of responsive behavior without any breakpoints. I'd reach for these before adding another media query.
Auto-Fit Grid
One line of CSS gives you a card grid that goes from one column to four as space allows:
/* Responsive card grid - no media queries needed! */
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 24px;
}
/* How it works:
- auto-fit: Creates as many columns as will fit
- minmax(280px, 1fr): Each column is at least 280px,
but grows equally to fill space
Result:
- On mobile (< 560px): 1 column
- On tablet (~840px): 2-3 columns
- On desktop (1200px+): 4+ columns
*/
auto-fit with minmax() adjusts the column count to the container width
Watch the minimum value. On a 320px phone with 16px padding on each side, the container is only 288px wide, so a minmax(300px, 1fr) column overflows and you get horizontal scrolling. minmax(min(280px, 100%), 1fr) avoids that.
Flexible Flexbox Navigation
This example uses a max-width query for the small-screen tweak. That's fine for a one-off adjustment, but if you find yourself writing lots of them, the base styles are probably desktop styles in disguise.
/* Responsive navigation that wraps naturally */
.nav {
display: flex;
flex-wrap: wrap;
gap: 16px;
justify-content: space-between;
align-items: center;
}
.nav-links {
display: flex;
flex-wrap: wrap;
gap: 8px 24px;
}
/* On mobile, nav-links can stack below logo */
@media (max-width: 640px) {
.nav {
flex-direction: column;
gap: 12px;
}
.nav-links {
justify-content: center;
}
}
Fluid Typography
Rather than bumping font sizes at every breakpoint, clamp() lets text scale smoothly between a minimum and a maximum:
/* Fluid typography using clamp() */
:root {
/* clamp(minimum, preferred, maximum) */
/* Headings */
--font-h1: clamp(2rem, 5vw + 1rem, 4rem);
--font-h2: clamp(1.5rem, 3vw + 0.75rem, 2.5rem);
--font-h3: clamp(1.25rem, 2vw + 0.5rem, 1.75rem);
/* Body text */
--font-body: clamp(1rem, 0.5vw + 0.875rem, 1.125rem);
--font-small: clamp(0.875rem, 0.5vw + 0.75rem, 1rem);
}
h1 { font-size: var(--font-h1); }
h2 { font-size: var(--font-h2); }
h3 { font-size: var(--font-h3); }
body { font-size: var(--font-body); }
small { font-size: var(--font-small); }
/* How clamp() works:
clamp(2rem, 5vw + 1rem, 4rem)
- Minimum: 2rem (32px) - never smaller
- Preferred: 5vw + 1rem - scales with viewport
- Maximum: 4rem (64px) - never larger
The browser picks the middle value, clamped
between min and max */
Keep a rem part in the middle value. A preferred size of pure vw ignores the user's font settings and barely changes when they zoom, which is an accessibility problem.
Apply the same technique to margins, padding, and gaps:
:root {
--space-sm: clamp(0.5rem, 1vw, 1rem);
--space-md: clamp(1rem, 2vw, 2rem);
--space-lg: clamp(2rem, 4vw, 4rem);
}
section { padding: var(--space-lg) var(--space-md); }
.card { padding: var(--space-md); gap: var(--space-sm); }
Mobile-First Best Practices
Design Mobile Wireframes First
If a layout works on a small screen, spreading it out for desktop is easy. Squeezing a desktop layout into a phone is much harder.
Content Priority
A small screen makes you decide what matters. Put that first. Secondary content can wait for wider screens or go further down the page.
Touch-Friendly Targets
Buttons and links should be at least 44×44px. Space interactive elements far enough apart to prevent accidental taps.
Test on Real Devices
DevTools device mode doesn't emulate a real phone's CPU, touch, or browser toolbars. On iOS Safari, for example, 100vh includes the area behind the address bar, so full-height sections get cut off. Use 100dvh (or svh) and check on an actual device.
Use Relative Units
Prefer rem, em, %, vw, and vh over fixed pixels. They scale better across devices and respect user font preferences.
Useful Tools & Resources
Browser DevTools
All major browsers have responsive design modes:
- Chrome/Edge: F12 → Toggle Device Toolbar (Ctrl+Shift+M)
- Firefox: F12 → Responsive Design Mode (Ctrl+Shift+M)
- Safari: Develop → Enter Responsive Design Mode
Also drag the width slowly through odd sizes like 700px or 900px, not just the device presets. Layouts usually break in the gaps between your breakpoints.
Performance Testing
- Lighthouse: Built into Chrome DevTools, audits performance and responsiveness
- PageSpeed Insights: Google's online tool for mobile/desktop performance
- WebPageTest: Advanced testing with real devices and network conditions
- Chrome UX Report: Real-world performance data from Chrome users
CSS Frameworks with Mobile-First
- Tailwind CSS: Mobile-first by default. Unprefixed utilities are mobile; use sm:, md:, lg: for larger screens
- Bootstrap 5: Mobile-first grid system with responsive breakpoint classes
- Foundation: Mobile-first with a flexible grid and responsive utilities
When mobile-first is worth it (and when it isn't)
For anything public facing, like marketing sites, blogs, stores, or signup flows, I'd start mobile-first every time. Your phone users are often on slower networks and weaker CPUs, and they're the ones who suffer most from a stylesheet full of overrides.
It's less clear-cut for internal tools. An admin panel with dense data tables that people only open on work laptops doesn't gain much from being designed phone-first. There I'd build for desktop, make sure nothing overflows horizontally on a small screen, and stop there. The same goes for retrofitting a big existing desktop-first codebase: flipping every max-width query is a lot of risk for little gain. Use min-width for new components and leave the rest alone.
The mistakes I see most often:
- Missing viewport meta tag, so the mobile styles never apply on real phones
- Mixing
min-widthandmax-widthin the same component until nobody knows which rule wins - Fixed widths (
width: 1200pxinstead ofmax-width) that cause horizontal scrolling - Hiding content with
display: noneand assuming it isn't downloaded. Images inside a hidden element may still be fetched.
Before you ship, open the page on a real phone over a throttled connection and scroll the whole thing. If nothing scrolls sideways and the main content shows up fast, you're in good shape.