Lesson 18 of 25

CSS Responsive Design & Media Queries

One Page That Fits Every Screen

Responsive design means a single set of HTML and CSS that reshapes itself for whatever screen it lands on — a phone held in one hand on a bus, a shared desktop in a college lab, a tablet, a projector. The alternative, a separate mobile site, means maintaining two of everything and choosing between them based on guesses about the device. Nobody does that any more, and for most of your visitors the phone is the only version they will ever see.

Before any CSS, one line of HTML has to be right. Without <meta name="viewport" content="width=device-width, initial-scale=1"> in the <head>, a mobile browser pretends to be about 980 pixels wide and then shrinks the whole rendered page to fit the screen. Your text becomes unreadably small, and your media queries never fire because the browser believes it is on a wide screen. If a site looks like a zoomed-out desktop page on a phone, this tag is missing.

The other half of the foundation is to stop declaring fixed widths. A container with width: 1100px cannot fit a 360-pixel phone and will force horizontal scrolling. max-width: 1100px says "never wider than this, but narrower is fine", which is what you actually mean. The same idea applies to images: max-width: 100% with height: auto stops a large photo bursting out of its container while keeping its proportions.

Example
<!-- Without this, nothing below matters -->
<meta name="viewport" content="width=device-width, initial-scale=1">

/* A container that shrinks but never overflows */
.container {
  max-width: 1100px;   /* not width: 1100px */
  margin-inline: auto;
  padding-inline: 20px;
}

/* Images that behave */
img {
  max-width: 100%;
  height: auto;
  display: block;
}
Notes
  • 100vh on a phone measures the viewport as if the browser's address bar were hidden, so a section sized that way is slightly taller than the visible area and the page scrolls a little when it should not. 100dvh — the dynamic viewport height — tracks the visible area as the bar appears and disappears, and is the better choice for full-screen sections.

Mobile-First: Why min-width Beats max-width

A media query wraps a block of rules in a condition. The rules inside apply only when the condition is true, and they follow the normal cascade — so a media query later in the file overrides the base rules it repeats.

There are two ways to organise them. Mobile-first writes the base rules for the smallest screen and adds complexity with min-width queries as space becomes available. Desktop-first writes the base rules for a wide screen and strips things back with max-width queries. Both work. Mobile-first is better, for three reasons that show up in every real project.

First, the base styles end up genuinely simple, because a phone layout is mostly a single column and you are describing that directly rather than undoing a multi-column layout. Second, you spend your time adding rules instead of overriding them, which keeps specificity flat and the file shorter. Third, a phone on a slow connection only has to apply the small base stylesheet rather than parse a desktop layout and then a set of rules cancelling it.

One detail prevents a genuinely confusing bug: if you mix both styles, do not put a max-width: 768px query and a min-width: 768px query in the same stylesheet. At exactly 768 pixels both conditions are true, both blocks apply, and whichever comes last wins — which is almost never what you intended. Pick one direction and stay with it.

Example
/* MOBILE-FIRST — base styles are the phone layout */
.layout {
  display: grid;
  grid-template-columns: 1fr;   /* one column */
  gap: 24px;
}

.nav-links { display: none; }   /* hidden behind a menu button */

/* Tablet and up: add a second column */
@media (min-width: 768px) {
  .layout { grid-template-columns: 240px 1fr; }
  .nav-links { display: flex; gap: 24px; }
  .menu-button { display: none; }
}

/* Desktop and up: more room again */
@media (min-width: 1100px) {
  .layout { grid-template-columns: 260px 1fr 300px; }
}

/* Combining conditions */
@media (min-width: 768px) and (max-width: 1099px) { /* tablet band only */ }
@media (max-width: 480px), print                  { /* small screens OR print */ }

/* The modern range syntax reads more like maths */
@media (width >= 768px) { /* same as min-width: 768px */ }
  • min-width — applies at that width and above; the mobile-first direction
  • max-width — applies at that width and below; the desktop-first direction
  • and requires both conditions; a comma means either
  • Never use the same number for a min-width and a max-width query
  • Media queries do not add specificity — they win by coming later in the file
  • Put your media queries after the rules they modify, or they will not apply

Breakpoints Belong to Your Content

Lists of "standard" breakpoints circulate widely — 576, 768, 992, 1200 — and they are a reasonable starting point, but they are not device sizes and chasing device sizes is a losing game. Phones, tablets and laptops overlap in width, new sizes appear constantly, and a window on a desktop can be any width its owner drags it to.

The reliable method takes about two minutes and needs no lists. Open your page, make the window as narrow as it goes, and widen it slowly. At some point the layout stops looking right — a headline breaks awkwardly, columns get too narrow to read, a row of buttons runs out of room. That width is your breakpoint. Add a media query there and carry on widening. Most pages need two or three, not seven.

Width is not the only thing you can ask about, and some of the other media features matter more than another breakpoint. prefers-reduced-motion tells you the visitor has asked for less movement. prefers-color-scheme tells you whether their system is in dark mode. hover and pointer tell you what kind of input device is in use — which matters because a menu that only opens on hover is unusable on a touchscreen, where there is no hover at all. Wrapping hover-dependent behaviour in @media (hover: hover) means touch users get a version that actually works.

There is also @media print, which most sites ignore and which takes five minutes to get right. Hiding the navigation, the sidebar and any buttons, and letting the article run at full width, turns a page that prints as a mess into one a student can print or save as a PDF for offline reading.

Example
/* Ask about intent and hardware, not just width */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}

@media (prefers-color-scheme: dark) {
  :root { --bg: #14141a; --text: #ececf2; }
}

/* Only devices that can genuinely hover */
@media (hover: hover) and (pointer: fine) {
  .card:hover { transform: translateY(-4px); }
}

/* Bigger tap targets where the pointer is a finger */
@media (pointer: coarse) {
  .nav a { padding: 14px 18px; }
}

/* Make the page printable */
@media print {
  .site-nav, .sidebar, .cookie-bar, .btn { display: none; }
  body { color: #000000; background: #ffffff; }
  .container { max-width: none; padding: 0; }
}
Notes
  • Test by dragging your browser window narrower rather than only using the device toolbar in developer tools. Continuous resizing shows you the widths where the layout actually breaks, which the fixed device presets will step straight over.

Layouts That Need Almost No Breakpoints

The best media query is the one you did not have to write. Several modern CSS features adapt on their own, and leaning on them keeps stylesheets dramatically shorter.

grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)) gives you a gallery that becomes four columns, then three, then two, then one, entirely by itself. flex-wrap: wrap with flex: 1 1 240px does the same job for a row of items. clamp() makes headings and spacing scale smoothly between a minimum and a maximum instead of jumping at a breakpoint. And max-width plus margin-inline: auto handles the whole page shell.

There is one situation these cannot solve, and it is worth naming because it explains a frustration you will meet. A media query asks about the screen, but a component often cares about the width of its own container. The same card might sit in a wide main area and in a narrow sidebar on the same page — and no media query can distinguish those, because the screen width is identical. Container queries solve exactly this: mark an element as a container with container-type: inline-size, and its children can then respond to that element's width using @container. For a component you intend to reuse in several places, that is a much more honest question to ask.

The demo below combines the pieces: a page shell with a max width, a navigation that switches from stacked to horizontal at a breakpoint, a gallery that reflows with no breakpoint at all, and a heading sized with clamp(). Drag the preview narrower and wider and watch which parts change smoothly and which snap.

Example
/* Reflows with no media query */
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 24px;
}

/* Fluid type, no breakpoint */
h1 { font-size: clamp(1.75rem, 1.2rem + 2.5vw, 3rem); }

/* Fluid spacing works the same way */
.section { padding-block: clamp(32px, 6vw, 96px); }

/* A component that responds to its own container, not the screen */
.card-area { container-type: inline-size; }

@container (min-width: 400px) {
  .card { display: grid; grid-template-columns: 120px 1fr; gap: 16px; }
}
A responsive page: drag the preview narrower and wider
HTML
<header class="bar">
  <div class="logo">Priodemy</div>
  <nav class="links">
    <a href="#">Courses</a>
    <a href="#">Practice</a>
    <a href="#">Certificates</a>
  </nav>
</header>

<main class="container">
  <h1>Learn CSS by building things</h1>
  <p class="lead">This heading uses clamp(), so it scales smoothly instead of jumping at a breakpoint. The cards below use auto-fit and need no media query at all.</p>

  <div class="gallery">
    <article class="card"><h2>Box model</h2><p>Content, padding, border, margin.</p></article>
    <article class="card"><h2>Flexbox</h2><p>One direction, shared space.</p></article>
    <article class="card"><h2>Grid</h2><p>Rows and columns together.</p></article>
    <article class="card"><h2>Positioning</h2><p>When flow is not enough.</p></article>
  </div>
</main>
CSS
*, *::before, *::after { box-sizing: border-box; }

body {
  margin: 0;
  font-family: system-ui, Arial, sans-serif;
  background: #f4f4f7;
  color: #222222;
}

/* --- MOBILE FIRST: the base styles are the phone layout --- */
.bar {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 16px 20px;
  background: #ffffff;
  border-bottom: 1px solid #e3e3ea;
}

.logo { font-weight: 700; color: #d1039e; }

.links { display: flex; gap: 16px; flex-wrap: wrap; }
.links a { color: #555555; text-decoration: none; }

.container {
  max-width: 1000px;
  margin-inline: auto;
  padding: clamp(20px, 4vw, 48px);
}

/* Fluid heading — no breakpoint involved */
h1 {
  font-size: clamp(1.6rem, 1.1rem + 2.4vw, 2.75rem);
  line-height: 1.2;
  margin: 0 0 8px;
}

.lead { color: #555555; max-width: 60ch; margin: 0 0 24px; }

/* Reflows by itself: 4 -> 3 -> 2 -> 1 columns */
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 16px;
}

.card {
  background: #ffffff;
  border: 1px solid #e3e3ea;
  border-radius: 12px;
  padding: 20px;
}

.card h2 { margin: 0 0 6px; font-size: 1.05rem; color: #d1039e; }
.card p  { margin: 0; font-size: 14px; color: #555555; }

/* --- The one breakpoint this page actually needs --- */
@media (min-width: 700px) {
  .bar {
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
  }
}

@media (hover: hover) {
  .card { transition: transform 0.2s ease-out; }
  .card:hover { transform: translateY(-4px); }
}
  • auto-fit with minmax() replaces a whole set of grid breakpoints
  • clamp() replaces breakpoints for font sizes and spacing
  • max-width and margin-inline: auto handle the page shell
  • Container queries let a component respond to its own width instead of the screen's
  • Aim for two or three breakpoints chosen by looking at your layout, not by naming devices
  • Always check a real phone if you can — emulators do not reproduce touch targets or scrolling feel
Ask AI