CSS Media Queries

A media query is a conditional block of CSS that only applies its rules when the browsing environment matches certain conditions — most commonly the width of the browser viewport, but also things like orientation, color scheme preference, and input type. Media queries are the mechanism that makes responsive design possible: instead of writing separate stylesheets for phones, tablets, and desktops, you write one stylesheet whose behavior adapts as the viewport changes. Understanding exactly when a media query matches, and how it interacts with the normal CSS cascade, is essential to building layouts that behave predictably at every screen size.

Note: This lesson assumes you already know CSS selectors, the box model, and basic layout with Flexbox/Grid. It focuses purely on the media query mechanism itself.

Overview / How it works

A media query is written with the @media rule, followed by one or more conditions, followed by a block of ordinary CSS rules in curly braces. The browser evaluates the condition continuously: on initial page load, whenever the viewport is resized, whenever the device orientation changes, and whenever the user prints the page or changes a system preference like dark mode. If the condition is true, every rule inside the block is added to the set of rules being applied to the document, exactly as if it had been written outside a media query — it participates in the cascade like anything else. If the condition becomes false (say, the window is resized back above a breakpoint), the rules inside stop applying and the browser falls back to whatever other rule would otherwise win.

This is a critical point that trips people up: a media query does not add specificity. A selector inside @media (max-width: 600px) { .box { color: red; } } has exactly the same specificity as if you’d written .box { color: red; } on its own. What determines whether it wins over a conflicting rule for the same property on the same element is the normal cascade — specificity first, and if specificity is tied, whichever rule appears later in source order wins, regardless of whether it’s inside a media query or not. This means the order in which you write your media queries in the stylesheet genuinely matters, which is a common source of subtle responsive bugs (see Common Mistakes below).

Media queries also drive what’s often called mobile-first design: you write your base, unconditional CSS for the smallest/simplest layout, then use min-width queries to progressively add complexity as more viewport space becomes available. The alternative, desktop-first, writes the full desktop layout as the base and uses max-width queries to strip things down for smaller screens. Both are valid; mixing them carelessly in the same stylesheet is where the ordering bugs tend to appear.

Syntax

The general shape of a media query is:

@media [not|only] <media-type> [and] (<media-feature>: <value>) {
  /* normal CSS rules go here */
}
  • media type — usually screen, print, or all (the default if omitted). speech also exists for screen readers. Older types like tty and tv were dropped from the spec and should not be used.
  • and — combines a media type or feature with another feature; all parts joined by and must be true for the query to match.
  • media feature — a condition in parentheses, like (min-width: 768px) or (prefers-color-scheme: dark). Almost every real-world media query is built from these.
  • not — negates the entire query (not just the feature immediately after it).
  • only — historically used to hide the query from very old browsers that only understood media types; harmless but unnecessary today.
  • comma ( , ) — acts as a logical or between two whole queries: @media (max-width: 600px), (min-width: 1200px) { ... } matches either condition.
Feature Example Matches when…
width / min-width / max-width (min-width: 768px) Viewport width is ≥ 768px
height / min-height / max-height (max-height: 500px) Viewport height is ≤ 500px
orientation (orientation: landscape) Viewport is wider than it is tall
aspect-ratio (min-aspect-ratio: 16/9) Viewport ratio meets the threshold
prefers-color-scheme (prefers-color-scheme: dark) The user’s OS/browser is set to dark mode
prefers-reduced-motion (prefers-reduced-motion: reduce) The user has requested less animation
hover (hover: hover) The primary input can hover (e.g. a mouse)
pointer (pointer: fine) The primary input is precise (mouse/stylus, not touch)

Examples

Example 1: A simple max-width breakpoint

Applies to a page with a <body> and a <p class="notice"> element:

body {
  background-color: #ffffff;
  color: #1a1a1a;
}

.notice {
  font-size: 1rem;
  padding: 1rem;
}

@media (max-width: 600px) {
  body {
    background-color: #1a1a1a;
    color: #ffffff;
  }

  .notice {
    font-size: 0.875rem;
    padding: 0.5rem;
  }
}

Result: On a wide window the page shows a white background with dark text and a roomy notice box. As soon as the viewport is resized to 600px wide or narrower, the browser re-evaluates the query, and the page instantly flips to a dark background with light text, with the notice box’s font and padding shrinking slightly — no reload needed.

This is the simplest and most common pattern: a single breakpoint that overrides a handful of properties for narrow viewports.

Example 2: Mobile-first responsive grid

.card-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
}

@media (min-width: 600px) {
  .card-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 900px) {
  .card-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

Result: On a phone-sized viewport, .card-grid stacks its children in a single column. Once the viewport reaches 600px, the first min-width query starts matching in addition to the base rule, and the grid switches to two equal columns. At 900px and above, the second query also matches, and — because it appears later in the source and has equal specificity — its three-column declaration wins, overriding the two-column one for that property.

This is the canonical mobile-first pattern: start simple, and add min-width queries in ascending order so each wider breakpoint builds on (and overrides) the previous one.

Example 3: Combining multiple features and modern range syntax

.sidebar {
  display: none;
}

@media (min-width: 768px) and (orientation: landscape) {
  .sidebar {
    display: block;
    width: 240px;
  }
}

@media (prefers-color-scheme: dark) {
  body {
    background-color: #121212;
    color: #f5f5f5;
  }
}

@media (hover: hover) and (pointer: fine) {
  .button:hover {
    background-color: #2563eb;
  }
}

@media (400px <= width <= 900px) {
  .container {
    padding-inline: 1rem;
  }
}

Result: The sidebar stays hidden until the viewport is at least 768px wide and in landscape orientation, at which point it appears as a 240px column. Independently, if the user's system is set to dark mode, the whole page background and text colors invert to a dark theme, regardless of viewport size. The button's hover color only ever applies on devices that report a real mouse-like pointer, so touchscreens never get a "stuck" hover state. Finally, the last query uses the modern range syntax (an alternative to writing min-width and max-width separately) to add horizontal padding only within a specific mid-size band.

How it works step by step / Under the hood

  1. When the page loads (and on every resize, orientation change, zoom, or relevant system-preference change), the rendering engine builds a fresh snapshot of the current environment: viewport width and height in CSS pixels, orientation, resolution, and any active user/OS preferences.
  2. For each @media rule in the stylesheet, the engine evaluates its condition against that snapshot, resolving and, comma (or), and not the same way you'd evaluate a boolean expression.
  3. Every rule whose media query currently evaluates to true is folded into the same cascade as all other applicable rules on the page — there is no separate "responsive" pass. Specificity and source order work exactly as they would without the media query wrapper.
  4. Because specificity is unaffected by nesting inside @media, two conflicting declarations of equal specificity are resolved purely by which one appears later in the stylesheet. This is why breakpoint order in your source file matters just as much as the breakpoint values themselves.
  5. On any subsequent environment change (e.g. the user resizes the browser or rotates their tablet), the engine re-evaluates every media query and triggers a re-layout/re-paint for any elements whose applicable rules changed — this is why responsive pages update live without a reload.

Common Mistakes

Mistake 1: Overlapping breakpoints in the wrong source order

Because equal-specificity conflicts are resolved by source order, writing breakpoints out of sequence can silently undo a more targeted rule:

@media (min-width: 600px) {
  .card { width: 50%; }
}
@media (min-width: 900px) {
  .card { width: 33%; }
}
@media (max-width: 1200px) {
  .card { width: 100%; }
}

At a 1000px viewport, all three queries are true. Because the max-width: 1200px rule appears last and has the same specificity, it wins, silently overriding the intended 33% width from the 900px breakpoint — even though that rule looks more specific to the reader. The fix is to keep breakpoints in one consistent direction (mobile-first: ascending min-width) and avoid mixing in max-width queries that overlap them:

.card {
  width: 100%;
}

@media (min-width: 600px) {
  .card {
    width: 50%;
  }
}

@media (min-width: 900px) {
  .card {
    width: 33%;
  }
}

Mistake 2: Omitting units on a length value

Media feature values that represent lengths require an explicit unit, just like anywhere else in CSS. A bare number is invalid and the entire query is discarded:

@media (min-width: 600) {
  .banner {
    display: none;
  }
}

Because 600 has no unit, the browser cannot parse the media feature and drops the whole rule, so .banner never gets hidden at any width. The fix is simply to include the unit:

@media (min-width: 600px) {
  .banner {
    display: none;
  }
}

Best Practices

  • Choose breakpoints based on where your content starts to look cramped or awkward, not around specific device widths — devices and their screen sizes change constantly, but your layout's natural pressure points don't.
  • Pick one direction (mobile-first with min-width is the modern default) and stay consistent throughout a stylesheet to avoid the source-order pitfall shown above.
  • Keep related breakpoints for the same component near each other in the file, in ascending order, so the cascade behavior is easy to read at a glance.
  • Use rem or em units for breakpoints when you want them to scale with the user's font-size preference, and reserve raw pixel values for cases where you specifically want a fixed viewport threshold.
  • Always add a (prefers-reduced-motion: reduce) query around non-essential animations and transitions as an accessibility baseline.
  • Test by resizing an actual browser window (or using dev tools' device toolbar) rather than only trusting values on paper — real content wrapping and image scaling often reveal a better breakpoint than a guess.
  • Combine media queries with intrinsic layout tools (clamp(), minmax(), flexible Grid tracks) so you need fewer hard breakpoints overall.

Practice Exercises

  1. Write a stylesheet where a <nav> element displays its links in a column by default, and switches to a row layout once the viewport reaches 768px or wider.
  2. Add a media query that gives the page a dark background and light text automatically when the user's OS is set to a dark color scheme, without affecting anything else.
  3. Given three overlapping min-width breakpoints at 500px, 800px, and 1100px that each set a different font-size on h1, predict (then test) which font-size applies at exactly 900px, and explain why using what you know about source order and specificity.

Summary

  • Media queries wrap ordinary CSS rules in a condition based on the viewport or user/device environment; the rules only apply while the condition is true.
  • Rules inside a media query have normal specificity — the query itself adds none — so cascade and source order still decide conflicts between matching rules.
  • min-width queries in ascending order support mobile-first design; max-width queries in descending order support desktop-first design; mixing the two carelessly causes overlap bugs.
  • Features like prefers-color-scheme, prefers-reduced-motion, hover, and pointer let you respond to user preferences and input capabilities, not just screen size.
  • Modern range syntax ((400px <= width <= 900px)) offers a more concise alternative to separate min-width/max-width queries in evergreen browsers.
  • Always include explicit units on length-based feature values — an unparsable media feature causes the entire query to be silently ignored.