5 min read
On this page

Responsive Design

Mobile-First: Start Small, Add Breakpoints

Responsive design means your site works well on every screen size. The mobile-first approach starts with the smallest screen and adds complexity as space allows. This is not just a convention -- it produces better CSS.

Starting small forces you to prioritize content. You figure out what matters before adding layout for larger screens.

/* Base styles: mobile (no media query) */
.card-grid {
  display: grid;
  gap: 16px;
}

/* Tablet and up */
@media (min-width: 768px) {
  .card-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* Desktop and up */
@media (min-width: 1024px) {
  .card-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

Media Queries: Use min-width

min-width is the mobile-first approach: styles apply above the breakpoint. max-width is desktop-first: styles apply below the breakpoint. Use min-width.

/* Mobile-first with min-width (recommended) */
.nav {
  flex-direction: column;  /* Stacked on mobile */
}

@media (min-width: 768px) {
  .nav {
    flex-direction: row;   /* Horizontal on tablet+ */
  }
}

The min-width approach means your base styles are the simplest. Each breakpoint adds layout rather than removing it. Desktop-first with max-width requires overriding desktop styles for mobile, producing more overrides.

Common Breakpoints

/* Small phones */
/* No query needed -- this is the default */

/* Large phones / Small tablets */
@media (min-width: 640px) { }

/* Tablets */
@media (min-width: 768px) { }

/* Laptops */
@media (min-width: 1024px) { }

/* Desktops */
@media (min-width: 1280px) { }

/* Large desktops */
@media (min-width: 1536px) { }

These are guidelines, not rules. Set breakpoints where your content breaks, not where specific devices start. If a layout looks awkward at 900px, add a breakpoint at 900px.

Fluid Typography with clamp()

Instead of changing font sizes at breakpoints, use clamp() for smooth scaling:

h1 {
  font-size: clamp(2rem, 1.5rem + 2.5vw, 4rem);
  /* Minimum 2rem, scales with viewport, maximum 4rem */
}

h2 {
  font-size: clamp(1.5rem, 1.2rem + 1.5vw, 2.5rem);
}

body {
  font-size: clamp(1rem, 0.9rem + 0.4vw, 1.125rem);
}

The formula clamp(min, preferred, max) works as:

  • Below a certain viewport width: uses min
  • At the target viewport width: uses preferred (which includes a vw component for scaling)
  • Above a certain viewport width: caps at max

No media queries. The text scales fluidly.

Responsive Images

Images are often the largest assets on a page. Serving a 2000px image to a 375px phone wastes bandwidth.

srcset: Resolution Switching

Provide multiple sizes and let the browser choose:

<img
  src="photo-800.jpg"
  srcset="
    photo-400.jpg 400w,
    photo-800.jpg 800w,
    photo-1200.jpg 1200w,
    photo-1600.jpg 1600w
  "
  sizes="
    (min-width: 1024px) 50vw,
    (min-width: 640px) 75vw,
    100vw
  "
  alt="Mountain landscape at sunset"
>
  • srcset lists available image files and their intrinsic widths
  • sizes tells the browser how wide the image will be displayed at different viewport widths
  • The browser combines these to pick the optimal file (considering pixel density too)

The picture Element: Art Direction

Use <picture> when you want different images (not just sizes) at different breakpoints:

<picture>
  <!-- Wide landscape crop for desktop -->
  <source
    media="(min-width: 1024px)"
    srcset="hero-wide-1200.jpg 1200w, hero-wide-1800.jpg 1800w"
    sizes="100vw"
  >
  <!-- Taller crop for mobile -->
  <source
    media="(min-width: 640px)"
    srcset="hero-medium-800.jpg 800w"
    sizes="100vw"
  >
  <!-- Square crop for small screens -->
  <img
    src="hero-square-400.jpg"
    alt="Product showcase"
    width="400"
    height="400"
  >
</picture>

Format Switching

Serve modern formats with fallbacks:

<picture>
  <source type="image/avif" srcset="photo.avif">
  <source type="image/webp" srcset="photo.webp">
  <img src="photo.jpg" alt="Description">
</picture>

AVIF and WebP are significantly smaller than JPEG at comparable quality.

Preventing Layout Shift

Always include width and height attributes on images:

<img src="photo.jpg" alt="Description" width="800" height="600">
img {
  max-width: 100%;
  height: auto;
}

The browser uses the width and height to calculate the aspect ratio before the image loads, preventing layout shift. Combined with max-width: 100% and height: auto, the image scales responsively.

Container Queries for Component-Level Responsiveness

Media queries respond to the viewport. Container queries respond to the component's container. This is the right tool for reusable components.

.card-wrapper {
  container-type: inline-size;
}

/* Card stacks vertically in narrow containers */
.card {
  display: flex;
  flex-direction: column;
}

/* Card switches to horizontal layout in wider containers */
@container (min-width: 500px) {
  .card {
    flex-direction: row;
    gap: 24px;
  }

  .card-image {
    flex: 0 0 200px;
  }
}

The same card component works in a sidebar (narrow, stacked) and the main content area (wide, horizontal) without any modifications. This is component-driven responsive design.

Viewport Units

Unit Meaning
vw 1% of viewport width
vh 1% of viewport height
dvh 1% of dynamic viewport height
svh 1% of small viewport height
lvh 1% of large viewport height
vmin 1% of the smaller viewport dimension
vmax 1% of the larger viewport dimension

The vh Problem on Mobile

On mobile browsers, 100vh does not equal the visible screen. The URL bar and bottom navigation take up space, making 100vh too tall.

/* Broken on mobile: content extends behind browser chrome */
.hero {
  height: 100vh;
}

/* Fixed: uses the dynamic viewport height */
.hero {
  height: 100dvh;
}
  • dvh changes as the browser chrome shows/hides (the address bar)
  • svh is the smallest possible viewport (browser chrome fully visible)
  • lvh is the largest possible viewport (browser chrome hidden)

Use dvh for full-screen sections to avoid the mobile vh bug.

Testing Responsiveness

Chrome DevTools Device Mode

  1. Open DevTools (F12 or Cmd+Shift+I)
  2. Click the device toggle icon (or Cmd+Shift+M)
  3. Select a device preset or enter custom dimensions
  4. Test at various widths by dragging the viewport handle

What to Check

  • Does content reflow correctly at every width?
  • Is text readable without horizontal scrolling?
  • Are touch targets at least 44x44 pixels on mobile?
  • Do images scale and not overflow?
  • Is the navigation usable on small screens?
  • Does the layout work in both portrait and landscape?

Test at key widths: 320px (smallest phones), 375px (standard phone), 768px (tablet), 1024px (laptop), 1280px (desktop), 1920px (large desktop).

Common Pitfalls

  • Using max-width media queries (desktop-first): This approach requires overriding desktop styles for mobile. min-width (mobile-first) adds layout progressively and produces cleaner CSS.
  • Breakpoints based on devices: Devices change constantly. Set breakpoints where your content needs them, not at arbitrary device widths.
  • Not testing between breakpoints: Your layout might look great at 375px and 768px but break at 500px. Resize continuously, not just to preset widths.
  • Using vh on mobile: 100vh is taller than the visible screen on mobile browsers. Use dvh for full-screen layouts.
  • Serving desktop images to mobile: A 2000px image on a 375px screen wastes bandwidth and slows load times. Use srcset and sizes.
  • Forgetting width and height on images: Without them, the browser cannot reserve space before the image loads, causing layout shift.
  • Over-relying on media queries: Many responsive patterns are achievable with clamp(), min(), auto-fit, and container queries -- no media queries needed.
  • Not using min() for container width: width: min(90%, 1200px) is cleaner than a max-width with separate width: 90%.

Key Takeaways

  • Mobile-first means starting with styles for small screens and adding layout with min-width media queries
  • Set breakpoints where your content breaks, not at specific device widths
  • clamp() enables fluid typography and spacing without media queries
  • Use srcset and sizes on images to serve appropriate sizes; use <picture> for art direction
  • Container queries make components responsive to their container, not the viewport
  • Use dvh instead of vh for full-screen sections on mobile
  • repeat(auto-fit, minmax(250px, 1fr)) creates responsive grids without media queries
  • Always include width and height on images to prevent layout shift
  • Test responsiveness by continuously resizing, not just at preset device widths