Media Queries

Session 8 — Responsive Design: One Site, Every Screen

60 minutes
📱 Responsive
🔧 Hands-On
Scroll to begin
Part 1 · Overview

Session Agenda

We'll go from the basics of media query syntax all the way to container queries and building a fully responsive layout — no frameworks, pure CSS.

0 – 10 min

Media Query Syntax

The @media rule, media features, logical operators, and the new range syntax.

10 – 20 min

Breakpoints & Mobile-First

Standard breakpoints, the mobile-first philosophy, and why min-width beats max-width.

20 – 35 min

Fluid Typography & Responsive Images

clamp() for font sizes that scale without media queries, aspect-ratio, object-fit, and viewport units.

35 – 55 min

Case Study — Responsive Card Layout

Build a complete page layout from mobile base styles up to a desktop 3-column grid, step by step.

55 – 60 min

Container Queries + Quiz

The modern evolution: @container lets components respond to their parent — not the viewport.

Part 2 · Media Queries

@media — CSS's Condition System

A media query is a condition written in CSS. When the condition is true, the rules inside apply. When it's false, they don't. The browser re-evaluates them every time the viewport changes.

media-query-anatomy.css
/*   @media  [type]  and  (feature)  { rules } */

@media screen and (min-width: 768px) {
  .container {
    max-width: 1200px;
  }
}
/*  └──┬──┘  └──┬──┘       └──────────────┘
       │         │               │
   at-rule   media type    media feature    */

Media types narrow down which output device the rules apply to:

TypeWhen it appliesUse case
all Always (default when omitted) General rules
screen Any screen device Web layouts, animations
print Printing / print preview Hide navbars, adjust fonts for paper

Media features are the conditions you test. The most common:

media-features.css
/* ── Viewport dimensions ── */
@media (min-width: 768px) { }   /* viewport ≥ 768px */
@media (max-width: 767px) { }   /* viewport < 768px */
@media (min-height: 500px) { }  /* tall viewports */

/* ── Device orientation ── */
@media (orientation: portrait)  { } /* taller than wide */
@media (orientation: landscape) { } /* wider than tall */

/* ── User preferences ── */
@media (prefers-color-scheme: dark)         { } /* user's OS is dark mode */
@media (prefers-reduced-motion: reduce)     { } /* user wants less animation */
@media (hover: hover)                       { } /* device has a real hover (mouse) */

/* ── Modern range syntax (cleaner) ── */
@media (width >= 768px) { }                  /* same as min-width: 768px */
@media (600px <= width <= 1024px) { }        /* range: tablet only */

/* ── Logical operators ── */
@media screen and (min-width: 768px) { }     /* AND — both must be true */
@media (max-width: 600px), (orientation: portrait) { } /* OR — comma = "or" */
@media not (prefers-color-scheme: dark) { }  /* NOT */
💡 You can skip the media type

Writing @media (min-width: 768px) without screen is perfectly valid and extremely common. The default is all, which covers screens, print, and everything else.

🧠 prefers-reduced-motion is important

Some users have vestibular disorders — spinning and sliding animations can cause physical discomfort. Wrapping all your animations in @media (prefers-reduced-motion: no-preference) (or disabling them inside a reduce block) is a simple, high-impact accessibility win.

Part 3 · Interactive

Breakpoints in Action

Click a device size below to see how the same HTML reflows. The live CSS panel shows which @media block is active at each size. This is exactly what the browser does as you resize the window.

Simulate viewport:
AboutWorkContact
Card One
Web Design
Card Two
Development
Card Three
Branding
Card Four
Motion
Card Five
UX/UI
Card Six
Strategy
📱 < 600px — Mobile base styles apply
/* Mobile — no @media needed, this IS the base */ .nav-links { display: none; } .hamburger { display: block; } .cards { grid-template-columns: 1fr; }

The key insight: the styles outside any @media block always apply. You write the mobile styles first (no query needed), then override with min-width queries for larger screens.

Here are the most widely used breakpoints — you'll see these everywhere:

Common CSS breakpoints (viewport width)
xs
<480
sm
480+
md
768+
lg
1024+
xl
1280+
2xl
1536+
0px 480px 768px 1024px 1280px 1536px
These are Tailwind's defaults. Bootstrap uses slightly different values (576, 768, 992, 1200). You can define your own — there's no universal standard.
🎯 Use as few breakpoints as possible

Don't add breakpoints at every device width. Add them only where your layout breaks. Resize your browser slowly and add a breakpoint exactly where things start to look bad. Two or three breakpoints handle 90% of real-world layouts.

Part 4 · Strategy

Mobile-First vs Desktop-First

There are two ways to use media queries. They produce the same visual result but have very different implications for code quality and performance.

✓ Mobile-First (preferred)

  • Write mobile styles as the base
  • Use min-width to add complexity
  • CSS cascades forward — simpler overrides
  • Mobile downloads the least CSS possible
  • Forces you to prioritize content
  • Google's mobile-first indexing

✗ Desktop-First (avoid)

  • Write desktop styles as the base
  • Use max-width to undo complexity
  • Mobile must download and override desktop CSS
  • Overrides pile up, specificity issues follow
  • Desktop gets the "real" design, mobile is an afterthought
  • Harder to maintain as the site grows
mobile-first.css — ✓ preferred approach
/* ── Base: mobile styles — NO query needed ── */
.cards {
  display: grid;
  grid-template-columns: 1fr;         /* single column */
  gap: 1rem;
}

.nav-links { display: none; }           /* hidden on mobile */
.hamburger { display: flex; }           /* shown on mobile */

/* ── Tablet: add complexity with min-width ── */
@media (min-width: 600px) {
  .cards     { grid-template-columns: 1fr 1fr; }
  .nav-links { display: flex; }
  .hamburger { display: none; }
}

/* ── Desktop: more complexity ── */
@media (min-width: 1024px) {
  .cards     { grid-template-columns: repeat(3, 1fr); }
  .container { max-width: 1200px; margin: 0 auto; }
}
desktop-first.css — ✗ avoid this pattern
/* ── Base: desktop styles ── */
.cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}
.nav-links { display: flex; }
.hamburger { display: none; }

/* ── Tablet: undo desktop rules ── */
@media (max-width: 1023px) {
  .cards { grid-template-columns: 1fr 1fr; }
}

/* ── Mobile: undo even more ── */
@media (max-width: 599px) {
  .cards     { grid-template-columns: 1fr; }    /* overriding again */
  .nav-links { display: none; }                 /* overriding again */
  .hamburger { display: flex; }                 /* overriding again */
}
/* Mobile had to download the desktop rules only to throw them away */
💡 The Golden Rule

Start simple. Add complexity. Mobile-first means your CSS reads as a progressive enhancement — from minimal to full-featured. This is also why min-width queries stack cleanly: each breakpoint only adds to what came before, never fights it.

Part 5 · Fluid Typography

clamp() — Fluid Sizing Without Queries

Fixed pixel font sizes look wrong on small or very large screens. Media queries help, but they create abrupt jumps. clamp() gives you smooth, fluid scaling with a guaranteed minimum and maximum.

1

The problem with fixed sizes

font-size: 3rem is 48px on every device — way too big on a 320px phone, maybe too small on a 2560px monitor.

2

vw units scale — but too aggressively

font-size: 5vw is fluid but has no floor or ceiling. On a 200px screen it's 10px (unreadable). On a 3000px monitor it's 150px (absurd).

3

clamp(min, preferred, max) — perfect

Returns the preferred value, but never goes below min or above max. The preferred value is usually a vw expression so it scales fluidly between the two bounds.

clamp-typography.css
/*  clamp(minimum, preferred, maximum)  */

.hero-title {
  /* Never smaller than 1.5rem (24px), never larger than 4rem (64px).
     Between those: scales with viewport at 5vw per 100px of width. */
  font-size: clamp(1.5rem, 5vw, 4rem);
}

.body-text {
  font-size: clamp(0.875rem, 2vw, 1.125rem); /* 14px → fluid → 18px */
}

.card-title {
  font-size: clamp(1rem, 3vw, 1.75rem);       /* 16px → fluid → 28px */
}

/* You can also mix in calc() for finer control */
.section-heading {
  font-size: clamp(1.5rem, 1rem + 3vw, 3rem);
  /* The "1rem + 3vw" ensures the font never drops below 1rem
     even at tiny viewports, because 1rem is always added */
}

/* clamp() works for spacing too, not just fonts */
.section {
  padding: clamp(2rem, 8vw, 6rem);           /* fluid vertical padding */
  max-width: clamp(300px, 90vw, 1200px);      /* responsive container width */
}

Interactive clamp() Demo

Drag the slider to simulate different viewport widths. The font size is computed from clamp(1rem, 4vw, 3rem) — watch the bounds clamp the value.

font-size: clamp(1rem, 4vw, 3rem)
↔ min 16px
↔ fluid zone (4vw)
↔ max 48px
Responsive
Viewport: 700px → 4vw = 28px → computed: 28px
🧠 min() and max() are also useful

min(4vw, 2rem) returns whichever is smaller — a cap with no floor. max(1rem, 4vw) returns whichever is larger — a floor with no ceiling. clamp() is just shorthand for max(min-value, min(preferred, max-value)).

Part 6 · Responsive Images

Images, Ratios & Viewport Units

Images have an intrinsic size. Without CSS, a 1200px-wide image simply overflows a 375px mobile screen. These properties fix that.

responsive-images.css
/* ── Rule #1: images never wider than their container ── */
img {
  max-width: 100%;    /* never overflow */
  height: auto;       /* keep aspect ratio */
  display: block;     /* remove inline-block gap under image */
}

/* ── aspect-ratio: lock proportions without hardcoding height ── */
.card-image {
  width: 100%;
  aspect-ratio: 16 / 9;  /* always 16:9, no matter the container width */
  object-fit: cover;      /* fill the box, crop edges */
}

.avatar {
  width: 3rem;
  aspect-ratio: 1;       /* perfect square (shorthand for 1/1) */
  border-radius: 50%;
  object-fit: cover;
}

/* ── object-fit controls image cropping ── */
.img-cover   { object-fit: cover; }    /* fill box, crop — most common */
.img-contain { object-fit: contain; }  /* fit inside box, show all */
.img-fill    { object-fit: fill; }     /* stretch to fill — distorts! */

object-fit Comparison

object-fit: fill
distorts the image
object-fit: contain
shows all, may letterbox
object-fit: cover
fills box, crops edges ✓

Viewport Units

viewport-units.css
/* ── Classic viewport units ── */
1vw  /* 1% of viewport width  */
1vh  /* 1% of viewport height */
1vmin /* 1% of the smaller of vw or vh */
1vmax /* 1% of the larger  of vw or vh */

.hero   { min-height: 100vh; }     /* full-screen hero */
.sidebar{ width: 25vw; }          /* quarter of viewport width */

/* ── Problem with 100vh on mobile ──
   On mobile browsers, the address bar eats into 100vh.
   The page is actually shorter than 100vh.          */

/* ── Modern fix: dynamic viewport units ── */
.hero { min-height: 100dvh; }  /* dvh = dynamic viewport height — adjusts for the address bar */
.hero { min-height: 100svh; }  /* svh = small viewport height — uses the smallest possible value */
.hero { min-height: 100lvh; }  /* lvh = large viewport height — ignores the address bar */

/* ── srcset for resolution switching ── */
/* In HTML, not CSS — loads the right-sized image per device */
responsive-image.html
<!-- srcset: browser picks the right size for the screen -->
<img
  src="hero-800.jpg"
  srcset="hero-400.jpg 400w,
          hero-800.jpg 800w,
          hero-1200.jpg 1200w"
  sizes="(min-width: 768px) 50vw, 100vw"
  alt="Hero image"
>

<!-- picture: art direction — different crops for different sizes -->
<picture>
  <source media="(min-width: 768px)" srcset="hero-wide.jpg">
  <source media="(min-width: 480px)" srcset="hero-square.jpg">
  <img src="hero-portrait.jpg" alt="Hero">  <!-- fallback -->
</picture>
💡 Use dvh for full-screen sections

Replace 100vh with 100dvh for hero sections and modals. It correctly accounts for the mobile browser address bar, which collapses and expands as the user scrolls — solving the classic "hero is too tall on mobile" bug.

Part 7 · Case Study

Building a Responsive Layout

Let's build a complete, responsive card page from scratch — mobile first. The HTML never changes. Only CSS changes at each breakpoint.

Step 1 Write the HTML structure

HTML is layout-agnostic. Write it in logical reading order — this is also the mobile order. No layout classes. No inline styles.

index.html
<header class="site-header">
  <a href="/" class="brand">Portfolio</a>
  <button class="hamburger" aria-label="Open menu"></button>
  <nav class="main-nav">
    <a href="#work">Work</a>
    <a href="#about">About</a>
    <a href="#contact">Contact</a>
  </nav>
</header>

<section class="hero">
  <h1>Creative <span>Portfolio</span></h1>
  <p>Design & development for the modern web.</p>
</section>

<main class="cards" id="work">
  <article class="card">
    <img src="project1.jpg" alt="Project One">
    <h2>Project One</h2>
    <p>E-commerce redesign, 2024.</p>
  </article>
  <!-- more cards... -->
</main>

Step 2 Mobile base styles — no media queries yet

Style for the smallest screen first. Single column, large touch targets, hamburger visible. This is the CSS that always loads.

styles.css — mobile base
.site-header {
  display: flex;
  align-items: center;
  padding: 1rem;
  background: #111;
  gap: 1rem;
}

.main-nav  { display: none; }    /* hidden on mobile */
.hamburger { display: block; }  /* shown on mobile */

.hero {
  padding: 2rem 1rem;
  text-align: center;
}

.hero h1 {
  font-size: clamp(2rem, 8vw, 4rem);  /* fluid title */
}

.cards {
  display: grid;
  grid-template-columns: 1fr;       /* single column */
  gap: 1.5rem;
  padding: 1rem;
}

.card img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 8px;
}

Step 3 Tablet breakpoint — 2 columns, show nav

styles.css — tablet breakpoint
@media (min-width: 600px) {
  .main-nav  { display: flex; gap: 1.5rem; margin-left: auto; }
  .hamburger { display: none; }

  .cards {
    grid-template-columns: 1fr 1fr;   /* 2 columns */
    padding: 1.5rem;
  }

  .hero { padding: 3rem 1.5rem; }
}

Step 4 Desktop breakpoint — 3 columns, constrain width

styles.css — desktop breakpoint
@media (min-width: 1024px) {
  .site-header,
  .hero,
  .cards {
    max-width: 1200px;    /* stop growing beyond 1200px */
    margin-inline: auto; /* center on wide screens */
  }

  .cards {
    grid-template-columns: repeat(3, 1fr);  /* 3 columns */
    padding: 2rem;
  }

  .hero {
    padding: 5rem 2rem;
    text-align: left;      /* left-aligned on large screens */
  }
}

Step 5 Accessibility — respect user motion preferences

styles.css — reduced motion
/* Animations for users who are OK with them */
.card { transition: transform 0.3s, box-shadow 0.3s; }
.card:hover { transform: translateY(-4px); }

/* Kill animations for users who prefer less motion */
@media (prefers-reduced-motion: reduce) {
  .card { transition: none; }
  .card:hover { transform: none; }

  /* Also reset any CSS animations globally */
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

Step 6 The finished result — all three sizes

Here is everything above combined. Toggle between sizes to see exactly how the CSS breakpoints transform the layout.

Active: mobile base (no @media)
Portfolio
Creative Portfolio
Design & development for the modern web.
Project One
E-commerce redesign
Project Two
Brand identity system
Project Three
Mobile app UI
🎯 The Pattern in One Line

Write the minimum viable layout for mobile with no queries. Then add @media (min-width: X) blocks to progressively enhance the layout for wider screens. Three sizes, two queries, done.

Part 8 · Modern CSS

@container — The Next Step

Media queries look at the viewport. But what if you have a reusable card component that lives in a narrow sidebar on one page and a wide main section on another? The viewport is the same — but the available space is very different.

The media query problem with reusable components
Sidebar (narrow)
Card.jsx
viewport = 1200px → but card is 200px wide!
Main content (wide)
Card.jsx — same component
viewport = 1200px → card is 700px wide ✓
A @media (min-width: 768px) would activate for both — even though the sidebar card is too narrow for a horizontal layout.

Container queries solve this. Instead of watching the viewport, the component watches its own containing element.

container-queries.css
/* Step 1: Tell the parent it's a container */
.card-wrapper {
  container-type: inline-size;  /* watch the inline (horizontal) size */
  container-name: card;          /* optional name for specificity */
}

/* Step 2: Style the child based on the CONTAINER width — not viewport */
.card {
  display: flex;
  flex-direction: column;   /* stacked by default (narrow container) */
}

@container card (min-width: 480px) {
  /* Only fires when the .card-wrapper is at least 480px wide */
  .card       { flex-direction: row; }        /* side-by-side */
  .card-image { width: 200px; flex-shrink: 0; }
}

/* The same component now adapts perfectly in sidebar AND main content */

Interactive Container Query Demo

Resize the container below. The card switches from vertical to horizontal layout at 480px of container width — not viewport width.

Container width:
Responsive Component
This card responds to its container width, not the viewport. Shrink the container and it stacks vertically. Widen it and it goes horizontal — regardless of what the rest of the page looks like.
💡 Container Types

container-type: inline-size — watches the horizontal size (most common). container-type: size — watches both width and height (rare, has performance implications). Never use a container as a flex or grid item directly — wrap it in an extra div if needed.

Feature@media@container
Responds toViewport sizeParent container size
Best forPage layout, global breakpointsReusable UI components
Use caseNav → hamburger, 1 col → 3 colsCard stack → horizontal, sidebar widget
Browser supportExcellent (all browsers)Good (all modern browsers since 2023)
Required setupNoneParent needs container-type
Part 9 · Wrap-Up

Recap & Quick Quiz

Let's review the key ideas from this session, then put them to the test.

📱

Mobile-First

Write the smallest layout first. Use min-width queries to layer on complexity for wider screens.

📏

@media Syntax

Combine media type + features with logical operators. Most queries only need (min-width: Xpx).

🔤

clamp(min, pref, max)

Fluid typography and spacing without media queries. The preferred value uses vw to scale smoothly.

📦

@container

Components that respond to their container's width — not the viewport. Perfect for reusable UI.

🧪 Quiz — Question 1 of 4

You're using the mobile-first approach. Which of these is the correct way to style a 2-column grid on tablet?

🧪 Quiz — Question 2 of 4

What does font-size: clamp(1rem, 5vw, 3rem) return on a 240px-wide viewport?

🧪 Quiz — Question 3 of 4

A reusable <Card> component needs to go horizontal when it has more than 500px of available width — whether that's in a sidebar or a main column. Which approach is correct?

🧪 Quiz — Question 4 of 4

Which CSS property ensures an image fills its container's width but never overflows it, while preserving its natural aspect ratio?
🎯 What's Next?

You now have the full CSS layout toolkit: Flexbox, Grid, and Responsive Design. In the next session we'll tackle state management — prop drilling, useContext, useReducer, and building a global store with zero external libraries.