Skip to content
beginner Phase 2 · HTML & CSS Foundations

Responsive Design

Build mobile-first responsive layouts with media queries, breakpoints, and fluid typography.

1h
0 problems
Topic Progress 0%

Viewport and Mobile-First Approach

Viewport and Mobile-First Approach

The Viewport Meta Tag

The viewport meta tag tells mobile browsers how to render the page. Without it, mobile browsers assume a desktop-width viewport (typically 980px) and scale the page down, making text tiny and unreadable.

<meta name="viewport" content="width=device-width, initial-scale=1.0">
  • width=device-width sets the layout viewport width to match the device's screen width
  • initial-scale=1.0 sets the initial zoom level to 100%

Common mistakes that harm accessibility:

  • maximum-scale=1.0 prevents users from zooming — this is an accessibility violation under WCAG 2.1 SC 1.4.4
  • user-scalable=no disables pinch-to-zoom entirely — a critical barrier for low-vision users

Why Mobile-First Strategy?

Mobile-first means writing base CSS for the smallest screens, then progressively enhancing for larger viewports with min-width media queries. This approach has three key advantages:

  1. Performance: Mobile devices receive only the essential CSS they need. Extra rules for larger screens are fetched only when the viewport is wide enough to need them.
  2. Progressive Enhancement: Starting simple and adding complexity prevents broken layouts on small screens. A desktop-first approach often leaves gaps when viewed on mobile.
  3. Browser Support: min-width queries are better supported in older browsers than max-width queries.

Mobile-First CSS Example

/* Base: mobile (single column) */
.card-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
  padding: 1rem;
}

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

/* Desktop: 3 columns */
@media (min-width: 1024px) {
  .card-grid {
    grid-template-columns: repeat(3, 1fr);
    max-width: 1200px;
    margin: 0 auto;
  }
}

The base styles define the mobile layout. Each min-width breakpoint adds capabilities for wider screens. This keeps the mobile experience lean and fast while enabling richer layouts on tablets and desktops.

Common Breakpoint Values

/* Mobile: 0 - 767px (base styles, no query needed)
   Tablet: 768px - 1023px */
@media (min-width: 768px) { /* ... */ }

/* Desktop: 1024px - 1439px */
@media (min-width: 1024px) { /* ... */ }

/* Large desktop: 1440px+ */
@media (min-width: 1440px) { /* ... */ }

Avoid targeting specific devices (iPhone, iPad). Instead, design for breakpoints that reflect your content's needs. Content will naturally break at different points depending on layout complexity.

Fluid Typography and Spacing

Fluid Typography and Spacing

Why Fluid Over Fixed?

Fixed font sizes create awkward gaps between breakpoints. A heading sized at 24px on mobile and 48px on desktop stays 24px until the exact breakpoint, then jumps to 48px. Fluid typography scales smoothly between a minimum and maximum size, eliminating these jumps entirely.

Fluid Type with clamp()

The clamp() function takes three arguments: a minimum value, a preferred value (often using viewport units), and a maximum value. It returns the preferred value as long as it stays within the min/max bounds.

html {
  /* Scales from 16px at 320px viewport to 20px at 1200px viewport */
  font-size: clamp(1rem, 0.875rem + 0.5vw, 1.25rem);
}

h1 {
  font-size: clamp(1.75rem, 1rem + 3vw, 3.5rem);
  line-height: 1.2;
  margin-bottom: 0.5em;
}

h2 {
  font-size: clamp(1.25rem, 1rem + 1.5vw, 2.25rem);
  line-height: 1.3;
}

p {
  font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
  line-height: 1.6;
}

The 0.875rem + 0.5vw portion is the preferred value — it grows with the viewport width. At 320px wide, this equals roughly 16px; at 1200px, it reaches about 20px. The browser handles the interpolation automatically.

Fluid Spacing Scale

Define a consistent spacing scale using CSS custom properties and clamp():

:root {
  --space-xs: clamp(0.25rem, 0.2rem + 0.25vw, 0.5rem);
  --space-sm: clamp(0.5rem, 0.4rem + 0.5vw, 1rem);
  --space-md: clamp(1rem, 0.8rem + 1vw, 2rem);
  --space-lg: clamp(2rem, 1.5rem + 2.5vw, 4rem);
  --space-xl: clamp(3rem, 2rem + 5vw, 6rem);
}

.section {
  padding: var(--space-lg) var(--space-md);
}

.card {
  padding: var(--space-sm);
  margin-bottom: var(--space-md);
}

This ensures spacing scales proportionally across all screen sizes without requiring media queries for every element.

Container Queries (Modern Component-Responsive Design)

Container queries let components respond to their parent container's width rather than the viewport. This makes components truly reusable across different layout contexts.

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

@container card (min-width: 400px) {
  .card {
    display: flex;
    gap: 1rem;
  }
  .card img {
    width: 200px;
    flex-shrink: 0;
  }
  .card-content {
    flex: 1;
  }
}

A card component might display stacked on mobile but side-by-side when its container is wide enough — regardless of whether that container is in a narrow sidebar or a wide main area.

Responsive Images and Media

Responsive Images and Media

The <picture> Element for Art Direction

The <picture> element lets you serve completely different images based on viewport width, format support, or pixel density. This is called art direction — showing a tightly cropped close-up on mobile and a wide landscape on desktop.

<picture>
  <source srcset="/images/hero-1200.webp" media="(min-width: 1024px)" type="image/webp">
  <source srcset="/images/hero-800.webp" media="(min-width: 640px)" type="image/webp">
  <source srcset="/images/hero-400.webp" type="image/webp">
  <img src="/images/hero-800.jpg" alt="Mountain landscape" 
       width="1200" height="600" loading="lazy">
</picture>

The browser evaluates <source> elements in order and uses the first matching one. The <img> fallback is required and is what screen readers and older browsers see.

Responsive <img> with srcset and sizes

The srcset attribute lets the browser choose the optimal image based on device pixel ratio and viewport width:

<img 
  src="/images/photo-800.jpg"
  srcset="/images/photo-400.jpg 400w,
          /images/photo-800.jpg 800w,
          /images/photo-1200.jpg 1200w"
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
  alt="Descriptive alt text"
  loading="lazy"
  decoding="async"
>
  • w descriptors tell the browser the actual pixel width of each image file
  • sizes tells the browser how wide the image will render at different viewport widths
  • The browser downloads the best match — a 2x Retina display at 640px viewport might select the 1200w image

Always include loading="lazy" for below-the-fold images and decoding="async" to avoid blocking the main thread.

Responsive Video with Aspect Ratio

Maintain consistent video proportions without relying on JavaScript or padding hacks:

.video-wrapper {
  position: relative;
  padding-bottom: 56.25%; /* 16:9 aspect ratio */
  height: 0;
  overflow: hidden;
}
.video-wrapper iframe {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

Or use the modern aspect-ratio property for native support:

.video-wrapper {
  aspect-ratio: 16 / 9;
  width: 100%;
}

Responsive Tables

Data tables break on narrow screens. Common solutions:

.table-wrapper {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

@media (max-width: 640px) {
  table { font-size: 0.875rem; }
  th, td { padding: 0.5rem; white-space: nowrap; }
}

For complex tables, consider redesigning the layout entirely — stack rows into card-like blocks on mobile rather than requiring horizontal scrolling.

Responsive Navigation Patterns

Responsive Navigation Patterns

CSS-Only Hamburger Toggle

A common pattern is showing horizontal navigation links on desktop and a hamburger menu on mobile. This can be done without JavaScript using a hidden checkbox:

<input type="checkbox" id="nav-toggle" class="nav-toggle-input" hidden>
<label for="nav-toggle" class="nav-toggle" aria-label="Toggle menu">
  <span></span>
</label>
<nav class="nav" aria-label="Main">
  <a href="/">Home</a>
  <a href="/about">About</a>
  <a href="/contact">Contact</a>
</nav>
.nav-toggle { display: none; cursor: pointer; }
.nav-toggle span,
.nav-toggle span::before,
.nav-toggle span::after {
  display: block; width: 24px; height: 2px;
  background: #333; position: relative;
  transition: transform 0.3s;
}

@media (max-width: 768px) {
  .nav-toggle { display: block; }
  .nav {
    position: fixed; top: 0; right: -100%;
    width: 280px; height: 100vh;
    background: white; flex-direction: column;
    transition: right 0.3s ease;
    box-shadow: -2px 0 8px rgba(0,0,0,0.1);
  }
  .nav-toggle-input:checked ~ .nav { right: 0; }
  .nav-toggle-input:checked ~ .nav-toggle span {
    transform: rotate(45deg);
  }
  .nav-toggle-input:checked ~ .nav-toggle span::before {
    opacity: 0;
  }
  .nav-toggle-input:checked ~ .nav-toggle span::after {
    transform: translateY(-8px) rotate(-90deg);
  }
}

Accessibility Considerations

Any responsive navigation must handle keyboard focus and ARIA states:

const toggle = document.querySelector('.nav-toggle');
const nav = document.querySelector('.nav');
const navLinks = nav.querySelectorAll('a');

toggle.addEventListener('click', () => {
  const isOpen = nav.classList.toggle('is-open');
  toggle.setAttribute('aria-expanded', isOpen);
  if (isOpen) navLinks[0].focus();
});

// Trap focus inside mobile nav when open
document.addEventListener('keydown', (e) => {
  if (!nav.classList.contains('is-open')) return;
  if (e.key !== 'Tab') return;
  const first = navLinks[0];
  const last = navLinks[navLinks.length - 1];
  if (e.shiftKey && document.activeElement === first) {
    last.focus(); e.preventDefault();
  } else if (!e.shiftKey && document.activeElement === last) {
    first.focus(); e.preventDefault();
  }
});

Always use aria-expanded to communicate the open/closed state to screen readers.

Skip to Content Link

A skip link improves keyboard navigation by letting users bypass the navigation entirely:

<a href="#main-content" class="skip-link">Skip to main content</a>

<style>
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: #000;
  color: white;
  padding: 8px;
  z-index: 100;
}
.skip-link:focus {
  top: 0;
}
</style>

Quiz

1. Why is the viewport meta tag important for mobile web development?

Question 1 options

2. Which CSS function allows you to create typography that scales smoothly between a minimum and maximum size based on viewport width?

Question 2 options

3. What is the key difference between media queries and container queries?

Question 3 options

Flashcards

Question

What does the viewport meta tag do and why should you avoid `user-scalable=no`?

Answer

The viewport meta tag tells mobile browsers how to render the page width and zoom level. `user-scalable=no` disables pinch-to-zoom, which is a WCAG accessibility violation (SC 1.4.4) that prevents low-vision users from enlarging content.

Question

Explain the three arguments of the `clamp()` function in fluid typography.

Answer

`clamp(minimum, preferred, maximum)` — The minimum is the smallest allowed value. The preferred value (often using vw units) scales with the viewport. The maximum is the largest allowed value. The browser returns the preferred value as long as it stays within bounds.

Question

Why use `min-width` media queries instead of `max-width` for mobile-first design?

Answer

Mobile-first design starts with base styles for small screens and progressively enhances for larger ones. `min-width` queries add styles as the viewport grows, ensuring mobile devices receive only essential CSS. `max-width` queries work in reverse (desktop-first), which can result in incomplete or broken mobile layouts if desktop styles aren't explicitly overridden.

Revision Notes

Key Takeaways

  • 1. Always include the viewport meta tag with `width=device-width, initial-scale=1.0` and never use `user-scalable=no` or `maximum-scale=1.0` as they violate accessibility standards
  • 2. Write mobile-first CSS using `min-width` media queries — start with base styles for small screens and progressively add complexity for larger viewports
  • 3. Use `clamp()` for fluid typography and spacing to create smooth scaling between minimum and maximum values without needing multiple breakpoints
  • 4. Use the `picture` element for art direction (different crops for different sizes) and `srcset` with `w` descriptors for resolution switching (same image at different pixel densities)
  • 5. Container queries (`container-type: inline-size`) let components respond to their parent container rather than the viewport, making them more reusable across different layout contexts

Interview Tips

  • When asked about responsive design, always mention the viewport meta tag first — it is the foundation without which no other technique works correctly on mobile
  • Explain mobile-first as a performance strategy, not just a layout convention — mobile devices receive minimal CSS upfront, reducing parse time and paint time
  • Be ready to compare `srcset` with the `picture` element: `srcset` handles resolution switching (same image, different sizes), while `picture` handles art direction (completely different images or crops)
  • Discuss container queries as the modern evolution of responsive components — they solve the problem where a card looks great in the main content but breaks in a sidebar
  • Know the accessibility implications: always allow user zoom, provide skip links for keyboard navigation, and use `aria-expanded` on toggle buttons

Cheat Sheet

Responsive Design Cheat Sheet

Viewport Meta Tag

<meta name="viewport" content="width=device-width, initial-scale=1.0">

Mobile-First Media Query

/* Base: mobile styles */
@media (min-width: 768px) { /* Tablet */ }
@media (min-width: 1024px) { /* Desktop */ }

Fluid Typography

font-size: clamp(1rem, 0.875rem + 0.5vw, 1.25rem);

Fluid Spacing

--space-md: clamp(1rem, 0.8rem + 1vw, 2rem);
padding: var(--space-md);

Container Query

.container { container-type: inline-size; }
@container (min-width: 400px) { /* styles */ }

Responsive Image

<img srcset="img-400.jpg 400w, img-800.jpg 800w"
     sizes="(max-width: 640px) 100vw, 50vw"
     src="img-800.jpg" alt="...">

Responsive Video

.video { aspect-ratio: 16 / 9; width: 100%; }

Common Breakpoints

  • Mobile: base (0–767px)
  • Tablet: min-width: 768px
  • Desktop: min-width: 1024px
  • Large: min-width: 1440px