Learn Responsive Design
82 patterns across 16 categories. Each one shows the convention, a side-by-side example, and why it matters.
Start here
New to responsive design? Follow these five categories in order.
Media Queries
Mobile-first breakpoints, logical ranges with min-width, and why max-width leads to override chains. You'll hit this when layouts break between standard device widths or when you inherit a desktop-first codebase.
/* Desktop-first: override down */
.container {
display: flex;
gap: 2rem;
}
@media (max-width: 768px) {
.container {
flex-direction: column;
gap: 1rem;
}
}/* Desktop-first: override down */
.container {
display: flex;
gap: 2rem;
}
@media (max-width: 768px) {
.container {
flex-direction: column;
gap: 1rem;
}
}/* Mobile-first: enhance up */
.container {
display: flex;
flex-direction: column;
gap: 1rem;
}
@media (min-width: 768px) {
.container {
flex-direction: row;
gap: 2rem;
}
}/* Mobile-first: enhance up */
.container {
display: flex;
flex-direction: column;
gap: 1rem;
}
@media (min-width: 768px) {
.container {
flex-direction: row;
gap: 2rem;
}
}Container Queries
@container vs @media: when components should own their own responsiveness instead of relying on the viewport. You'll hit this when a reusable component looks wrong after being placed in a narrower sidebar or modal.
/* Card adapts to viewport width */
.card {
display: flex;
flex-direction: column;
}
@media (min-width: 600px) {
.card {
flex-direction: row;
}
}/* Card adapts to viewport width */
.card {
display: flex;
flex-direction: column;
}
@media (min-width: 600px) {
.card {
flex-direction: row;
}
}/* Card adapts to its own container */
.card-wrapper {
container-type: inline-size;
}
.card {
display: flex;
flex-direction: column;
}
@container (min-width: 400px) {
.card {
flex-direction: row;
}
}/* Card adapts to its own container */
.card-wrapper {
container-type: inline-size;
}
.card {
display: flex;
flex-direction: column;
}
@container (min-width: 400px) {
.card {
flex-direction: row;
}
}Fluid Typography
Using clamp() and fluid type scales so text adapts smoothly without hard breakpoints. You'll hit this when headings look too big on mobile or too small on ultrawide monitors.
h1 {
font-size: 1.5rem;
}
@media (min-width: 768px) {
h1 { font-size: 2rem; }
}
@media (min-width: 1024px) {
h1 { font-size: 2.5rem; }
}
@media (min-width: 1280px) {
h1 { font-size: 3rem; }
}h1 {
font-size: 1.5rem;
}
@media (min-width: 768px) {
h1 { font-size: 2rem; }
}
@media (min-width: 1024px) {
h1 { font-size: 2.5rem; }
}
@media (min-width: 1280px) {
h1 { font-size: 3rem; }
}h1 {
font-size: clamp(1.5rem, 1rem + 2vw, 3rem);
}h1 {
font-size: clamp(1.5rem, 1rem + 2vw, 3rem);
}Viewport Units
vw, vh, dvh, svh, and lvh. Understanding the mobile viewport trap and picking the right unit. You'll hit this when a full-screen hero section hides behind the mobile browser toolbar.
.hero {
height: 100vh;
}.hero {
height: 100vh;
}.hero {
height: 100dvh;
}.hero {
height: 100dvh;
}Flexbox Patterns
flex-wrap, gap, shrink, and grow for layouts that reflow naturally across screen sizes. You'll hit this when items squish into a single row on small screens instead of wrapping naturally.
.card-row {
display: flex;
gap: 1rem;
}
@media (max-width: 768px) {
.card-row {
flex-direction: column;
}
}.card-row {
display: flex;
gap: 1rem;
}
@media (max-width: 768px) {
.card-row {
flex-direction: column;
}
}.card-row {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.card-row > * {
flex: 1 1 300px;
}.card-row {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.card-row > * {
flex: 1 1 300px;
}Grid Patterns
auto-fit, auto-fill, and minmax() for responsive grids that need zero media queries. You'll hit this when a card grid should go from one column to three without writing any breakpoint.
.grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 1rem;
}
@media (max-width: 768px) {
.grid {
grid-template-columns: 1fr;
}
}.grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 1rem;
}
@media (max-width: 768px) {
.grid {
grid-template-columns: 1fr;
}
}.grid {
display: grid;
grid-template-columns:
repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
}.grid {
display: grid;
grid-template-columns:
repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
}Responsive Spacing
Consistent spacing systems that scale with the viewport using clamp(), custom properties, and theme tokens. You'll hit this when padding and margins feel too tight on mobile or too loose on desktop.
.section {
padding: 2rem;
}
@media (min-width: 768px) {
.section { padding: 4rem; }
}
@media (min-width: 1024px) {
.section { padding: 6rem; }
}.section {
padding: 2rem;
}
@media (min-width: 768px) {
.section { padding: 4rem; }
}
@media (min-width: 1024px) {
.section { padding: 6rem; }
}.section {
padding: clamp(2rem, 1rem + 4vw, 6rem);
}.section {
padding: clamp(2rem, 1rem + 4vw, 6rem);
}Overflow Handling
Scroll containers, text truncation, responsive tables, and preventing horizontal overflow on mobile. You'll hit this when a table or long URL causes horizontal scrolling on mobile.
.list-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.list-item-label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}.list-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.list-item-label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}.list-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.list-item-label {
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}.list-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.list-item-label {
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}Breakpoint Hooks
useMediaQuery, custom breakpoint hooks, SSR hydration pitfalls, and when to avoid them entirely. You'll hit this when a component flickers after server-side rendering because useMediaQuery runs too late.
function App() {
const isMobile = useMediaQuery("(max-width: 768px)");
return isMobile
? <MobileLayout />
: <DesktopLayout />;
}function App() {
const isMobile = useMediaQuery("(max-width: 768px)");
return isMobile
? <MobileLayout />
: <DesktopLayout />;
}function App() {
return (
<>
<Box sx={{ display: { xs: "block", md: "none" } }}>
<MobileLayout />
</Box>
<Box sx={{ display: { xs: "none", md: "block" } }}>
<DesktopLayout />
</Box>
</>
);
}function App() {
return (
<>
<Box sx={{ display: { xs: "block", md: "none" } }}>
<MobileLayout />
</Box>
<Box sx={{ display: { xs: "none", md: "block" } }}>
<DesktopLayout />
</Box>
</>
);
}Responsive Props
Components that accept breakpoint-aware prop objects like direction={{ xs: 'column', md: 'row' }}. You'll hit this when you want a Stack to be vertical on mobile and horizontal on desktop in one prop.
function Features() {
const isMobile = useMediaQuery("(max-width: 600px)");
return (
<Stack direction={isMobile ? "column" : "row"}>
<FeatureCard />
<FeatureCard />
</Stack>
);
}function Features() {
const isMobile = useMediaQuery("(max-width: 600px)");
return (
<Stack direction={isMobile ? "column" : "row"}>
<FeatureCard />
<FeatureCard />
</Stack>
);
}function Features() {
return (
<Stack direction={{ xs: "column", sm: "row" }}>
<FeatureCard />
<FeatureCard />
</Stack>
);
}function Features() {
return (
<Stack direction={{ xs: "column", sm: "row" }}>
<FeatureCard />
<FeatureCard />
</Stack>
);
}Conditional Rendering
Rendering different components by viewport vs hiding with CSS, including performance and SEO tradeoffs. You'll hit this when you conditionally render with JavaScript but the hidden component still makes a network request.
function Nav() {
const isMobile = useMediaQuery("(max-width: 768px)");
return isMobile
? <MobileNav />
: <DesktopNav />;
}function Nav() {
const isMobile = useMediaQuery("(max-width: 768px)");
return isMobile
? <MobileNav />
: <DesktopNav />;
}function Nav() {
return (
<>
<Box sx={{ display: { xs: "block", md: "none" } }}>
<MobileNav />
</Box>
<Box sx={{ display: { xs: "none", md: "block" } }}>
<DesktopNav />
</Box>
</>
);
}function Nav() {
return (
<>
<Box sx={{ display: { xs: "block", md: "none" } }}>
<MobileNav />
</Box>
<Box sx={{ display: { xs: "none", md: "block" } }}>
<DesktopNav />
</Box>
</>
);
}Responsive Images
srcSet, sizes, next/image, art direction with <picture>, and avoiding layout shift. You'll hit this when a 2 MB hero image loads on a phone with a slow connection.
<img
src="/hero.jpg"
alt="Hero image"
style={{ width: "100%", height: "auto" }}
/><img
src="/hero.jpg"
alt="Hero image"
style={{ width: "100%", height: "auto" }}
/><img
src="/hero.jpg"
alt="Hero image"
width={1200}
height={630}
style={{ width: "100%", height: "auto" }}
/><img
src="/hero.jpg"
alt="Hero image"
width={1200}
height={630}
style={{ width: "100%", height: "auto" }}
/>MUI Responsive
MUI's sx breakpoint syntax, responsive Grid2, theme.breakpoints, and responsive dialog patterns. You'll hit this when you need an MUI dialog to be fullscreen on mobile but a centered modal on desktop.
<Typography
sx={{
fontSize: window.innerWidth < 600
? "1rem"
: "1.5rem",
}}
>
Welcome back
</Typography><Typography
sx={{
fontSize: window.innerWidth < 600
? "1rem"
: "1.5rem",
}}
>
Welcome back
</Typography><Typography
sx={{
fontSize: { xs: "1rem", sm: "1.5rem" },
}}
>
Welcome back
</Typography><Typography
sx={{
fontSize: { xs: "1rem", sm: "1.5rem" },
}}
>
Welcome back
</Typography>Tailwind Responsive
Utility-first responsive design with sm:/md:/lg: prefixes, container queries plugin, and custom screens. You'll hit this when Tailwind's sm: prefix doesn't behave the way you expected at 640px.
<div className="flex-row md:flex-row sm:flex-col">
<Card />
<Card />
</div><div className="flex-row md:flex-row sm:flex-col">
<Card />
<Card />
</div><div className="flex flex-col sm:flex-row">
<Card />
<Card />
</div><div className="flex flex-col sm:flex-row">
<Card />
<Card />
</div>Common Mistakes
Fixed widths, pixel assumptions, forgotten touch targets, ignoring landscape, and other responsive pitfalls. You'll hit this when a button is too small to tap on a phone or a layout collapses in landscape mode.
.container {
width: 960px;
margin: 0 auto;
}.container {
width: 960px;
margin: 0 auto;
}.container {
width: min(100% - 2rem, 960px);
margin-inline: auto;
}.container {
width: min(100% - 2rem, 960px);
margin-inline: auto;
}Testing Responsive
Strategies for testing responsiveness: viewport meta, device mode, visual regression, and automated checks. You'll hit this when a layout looks fine in Chrome DevTools but breaks on a real device.
// Only tested in Chrome DevTools device mode
// "Looks good at iPhone 14 Pro dimensions"
// Issues missed:
// - Touch interactions (hover states)
// - Safe area insets (notch)
// - Mobile browser chrome (URL bar height)
// - Virtual keyboard behavior
// - Scroll performance// Only tested in Chrome DevTools device mode
// "Looks good at iPhone 14 Pro dimensions"
// Issues missed:
// - Touch interactions (hover states)
// - Safe area insets (notch)
// - Mobile browser chrome (URL bar height)
// - Virtual keyboard behavior
// - Scroll performance// Multi-layer testing strategy:
// 1. Chrome DevTools for quick iteration
// 2. Responsive viewer for side-by-side comparison
// 3. BrowserStack/real devices for:
// - Touch interaction testing
// - Safari-specific CSS bugs
// - Virtual keyboard behavior
// - Performance on low-end devices
// - Safe area inset verification// Multi-layer testing strategy:
// 1. Chrome DevTools for quick iteration
// 2. Responsive viewer for side-by-side comparison
// 3. BrowserStack/real devices for:
// - Touch interaction testing
// - Safari-specific CSS bugs
// - Virtual keyboard behavior
// - Performance on low-end devices
// - Safe area inset verification