Skip to main content

Responsive Design With CSS Media Queries: Breakpoints, Mobile-First, and Feature Queries

A practical guide to CSS media queries — common breakpoints, mobile-first min-width vs max-width, prefers-color-scheme dark mode, and other feature queries.

Published By Li Lei
#css #responsive-design #media-queries #frontend

Responsive Design With CSS Media Queries: Breakpoints, Mobile-First, and Feature Queries

A media query is the one piece of CSS that lets a single stylesheet serve a phone, a tablet, and a 27-inch monitor without three separate codebases. You write a rule, wrap it in a condition, and the browser applies it only when the condition holds. Most of the time that condition is the viewport width, but the modern media spec reaches a lot further than width — into color scheme, motion preference, pointer type, and more.

This post walks through the parts of media queries I reach for on real projects: how breakpoints actually behave, why mobile-first prefers min-width, and the feature queries that have nothing to do with screen size at all.

How a width breakpoint actually behaves

Here is the single most useful fact about width queries, and the one people get wrong most often: a min-width query applies its styles at and above that width. @media (min-width: 768px) is active at 768px, at 769px, and at 2560px. It is not active at 767px. A max-width query is the mirror image — it applies at and below the stated width.

That "at and above" detail is what makes breakpoints stack cleanly. Each min-width query you add raises the floor; styles only ever get added as the screen grows, never undone. If you internalize that one rule, most layout bugs about "why is this style showing on the wrong size" answer themselves.

The classic trap is writing two adjacent queries that share a number:

@media (max-width: 768px) { /* tablet and down */ }
@media (min-width: 768px) { /* tablet and up */ }

A viewport sitting at exactly 768px satisfies both, so both blocks fire and the cascade order decides the winner — usually not what you intended. The fix is to cap the max-width query just below the boundary: (max-width: 767.98px). That 0.02px gap is the smallest fraction browsers reliably distinguish, and it is the exact value Bootstrap 5 ships.

Common breakpoints and where they come from

There is no law about which pixel values to break at, but the major frameworks have converged on a small set worth knowing:

  • Tailwind: sm 640, md 768, lg 1024, xl 1280, 2xl 1536
  • Bootstrap 5: sm 576, md 768, lg 992, xl 1200, xxl 1400
  • Material 3: medium 600, expanded 840, large 1200, extra-large 1600

Notice 768 shows up in two of the three — it tracks the long lineage of the iPad's portrait width and has become a de facto tablet threshold. The smart move is not to memorize these but to pick one system and keep your hand-written CSS aligned to it. If your custom rules break at 1024 but your utility classes break at 1023.98, components reflow at slightly different moments and the layout looks subtly broken in the seam between them. The CSS Media Query Generator emits a full skeleton for any of these presets so the two never drift, and if you keep the matching utility classes handy a Tailwind cheatsheet lines up directly against the same numbers.

Mobile-first: stacking min-width queries upward

Mobile-first is a strategy, not a syntax. It means your unprefixed base styles target the smallest screen, and every min-width query layers an enhancement on top as the viewport grows. You stack the queries upward: small base, then min-width: 640px, then min-width: 1024px, each one only adding.

Desktop-first inverts the whole thing — base styles assume a large screen, and each max-width query strips things back down for smaller viewports. Both work, but mixing them in one sheet is where dead zones and double-matched ranges creep in, so commit to one per stylesheet.

Here is a worked mobile-first example. A card grid that stacks in a single column on phones and widens to three columns at the desktop breakpoint:

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

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

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

Read it top to bottom and the layout only ever gains columns. At 500px you get the base one-column rule. At 800px the first query has kicked in for two columns. At 1400px both queries are active and the last one wins with three. Nothing is undone; the cascade is predictable. If you are sketching the grid itself before wrapping it in queries, a visual CSS grid generator gets the grid-template right first, and then the breakpoints just swap the column count.

px or em for the breakpoint value

px is what most teams ship and it is the simplest to reason about. But em units in a media query have one genuine advantage for accessibility: they are relative to the browser's default font size, so if a user bumps their default up for readability, the breakpoints shift with them. The layout breaks to the next column count at a point that tracks their text size instead of a fixed pixel.

One gotcha worth burning into memory: em in a media query is always relative to the browser default (16px by default), never to your :root { font-size } override. So 48em is 768px regardless of what you set in CSS. If you want component-relative scaling, that is container queries, a different feature entirely. When I need to sanity-check what an em breakpoint resolves to in pixels, a quick px/rem converter saves the mental arithmetic.

Feature queries beyond width

Width is where most people stop, but the queries that have improved my work the most lately are not about size at all.

prefers-color-scheme reads the operating system's light/dark setting. Drop your dark-theme variable overrides inside it and the browser applies them whenever the OS is in dark mode — no JavaScript, no class toggling, no flash of the wrong theme on first paint:

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0e0e10;
    --fg: #f4f4f5;
  }
}

The first time I added a single block like that to a marketing page, the dark-mode support went from "a script we keep meaning to write" to two minutes of work and zero runtime cost. It only reads the system preference, though — an in-app manual toggle still needs a class or data-attribute on the root.

prefers-reduced-motion lets you collapse animations to a static state for users with vestibular disorders, which closes a real WCAG 2.3.3 finding. pointer: coarse detects touch input so you can enlarge tap targets for thumbs, and hover: none hides hover-only affordances like tooltips on touchscreens. There is also orientation, min-aspect-ratio, and more — the whole feature space is bigger than the width axis everyone fixates on.

Putting it together

The mental model that makes media queries click: a min-width query applies at and above a width, mobile-first stacks those queries upward so styles only ever get added, and feature queries like prefers-color-scheme extend the same @media syntax to things that have nothing to do with size. Pick one strategy per sheet, align your breakpoints to whatever framework you already use, and reach for feature queries when the question is about the user's device or preference rather than their screen width. Generate the skeleton, paste your rules into the blocks, and the structure is correct from the first line.


Made by Toolora · Updated 2026-06-13