TL;DR
- Design mobile-first: start with the smallest screen, then enhance with min-width media queries
- Use CSS Grid auto-fill/auto-fit and Flexbox wrap for intrinsically responsive layouts
- Fluid typography with clamp() eliminates most font-size breakpoints
- Container queries let components respond to their parent's width, not the viewport
Mobile-First Is Not a Suggestion
Mobile-first isn't about designing for phones first — it's about progressive enhancement. Start with the most constrained environment and add complexity for larger screens.
In CSS, this means using min-width media queries (adding styles as screens grow) rather than max-width (removing styles as screens shrink).
/* ✅ Mobile-first: base is mobile, enhance upward */
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
@media (min-width: 640px) {
.grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
.grid { grid-template-columns: repeat(3, 1fr); }
}
/* ❌ Desktop-first: base is desktop, degrade downward */
.grid {
grid-template-columns: repeat(3, 1fr);
}
@media (max-width: 1023px) {
.grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 639px) {
.grid { grid-template-columns: 1fr; }
}
Breakpoint Strategy
Don't tie breakpoints to specific devices. Phones change size every year. Instead, use content-driven breakpoints:
:root {
/* Content-driven breakpoints */
--bp-sm: 640px; /* ~40rem — single column feels cramped */
--bp-md: 768px; /* ~48rem — room for two columns */
--bp-lg: 1024px; /* ~64rem — room for sidebar + content */
--bp-xl: 1280px; /* ~80rem — wide layouts */
}
Test by resizing your browser and adding breakpoints where the layout breaks, not at 375px because "that's an iPhone."
Intrinsic Design: Beyond Breakpoints
The best responsive code uses as few media queries as possible. CSS Grid and Flexbox can handle most responsive behavior intrinsically:
/* Responsive card grid — ZERO media queries */
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
gap: 1.5rem;
}
/* Responsive sidebar — collapses naturally */
.layout {
display: flex;
flex-wrap: wrap;
gap: 2rem;
}
.sidebar {
flex: 1 1 250px; /* grows from 250px, wraps when too narrow */
max-width: 350px;
}
.main {
flex: 999 1 0%; /* takes most space, allows sidebar to wrap */
min-width: 60%;
}
Fluid Typography
Instead of changing font-size at every breakpoint, use clamp() for smooth scaling:
/* Fluid type scale */
:root {
--text-sm: clamp(0.875rem, 0.8rem + 0.2vw, 1rem);
--text-base: clamp(1rem, 0.9rem + 0.3vw, 1.125rem);
--text-lg: clamp(1.25rem, 1rem + 0.6vw, 1.5rem);
--text-xl: clamp(1.5rem, 1.1rem + 1vw, 2rem);
--text-2xl: clamp(2rem, 1.3rem + 1.8vw, 3rem);
--text-3xl: clamp(2.5rem, 1.5rem + 2.5vw, 4rem);
}
h1 { font-size: var(--text-3xl); }
h2 { font-size: var(--text-2xl); }
h3 { font-size: var(--text-xl); }
p { font-size: var(--text-base); }
Container Queries
Media queries respond to the viewport. Container queries respond to the parent element's size. This is game-changing for reusable components:
.card-container {
container-type: inline-size;
container-name: card;
}
.card {
display: grid;
gap: 1rem;
}
/* When the CONTAINER (not viewport) is wide enough */
@container card (min-width: 400px) {
.card {
grid-template-columns: 150px 1fr;
}
}
@container card (min-width: 600px) {
.card {
grid-template-columns: 200px 1fr auto;
}
}
Container queries have excellent browser support (all modern browsers since early 2023). Use them.
Responsive Images
<!-- Art direction: different crops for different sizes -->
<picture>
<source media="(min-width: 1024px)" srcset="hero-wide.webp">
<source media="(min-width: 640px)" srcset="hero-medium.webp">
<img src="hero-mobile.webp" alt="Hero image" loading="lazy">
</picture>
<!-- Resolution switching: same image, different sizes -->
<img
src="photo-400.webp"
srcset="photo-400.webp 400w, photo-800.webp 800w, photo-1200.webp 1200w"
sizes="(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw"
alt="Product photo"
loading="lazy"
>
Best Practices
- Do design mobile-first and use min-width media queries
- Do use intrinsic sizing (auto-fill, minmax, flex-wrap) before reaching for media queries
- Do use clamp() for fluid typography and spacing
- Do test on real devices, not just browser resize — touch targets, font rendering, and performance differ
- Don't target specific device widths — use content-driven breakpoints
- Don't hide content on mobile that exists on desktop — rethink the hierarchy instead
- Don't forget <meta name="viewport" content="width=device-width, initial-scale=1"> — without it, nothing works
Common Mistakes
- Designing desktop-first then trying to "make it fit" on mobile — always harder than mobile-first
- Too many breakpoints — if you have 8+ breakpoints, your layout probably isn't intrinsically responsive enough
- Fixed widths in flexible containers — use max-width or min() instead of width
- Forgetting landscape phones — a phone in landscape is ~640-900px wide, which triggers "tablet" breakpoints
Tools to Explore
- Responsive Tester — preview your site across device sizes
- Grid Generator — build responsive grid layouts
- Flexbox Playground — experiment with wrapping and responsive flex layouts
- Fluid Calculator — generate fluid type and spacing scales