Media queries: width, orientation and user preferences

Write media queries with classic and range syntax, combine conditions, and respond to dark mode, reduced motion and input type.

  • Beginner
  • 9 min read
  • Updated 17 Oct 2026

What you will learn

  • Write width and range media queries
  • Respond to user preferences
  • Combine conditions with and, not and commas
  • Use media queries in HTML and JavaScript

Before you start

A media query applies CSS only when a condition about the device or the user's environment is true. This lesson covers everything you need: the syntax, width and range queries, orientation, input type, and the user preference queries that make sites more comfortable and accessible.

Anatomy of a media query

CSS
@media screen and (width >= 48rem) {
  .layout {
    grid-template-columns: 1fr 18rem;
  }
}

A media query has up to three parts:

PartExampleMeaning
Media typescreenThe kind of output: screen, print or all
Media feature(width >= 48rem)A testable condition, always in parentheses
Logical operatorsand, not, ,, onlyCombine or negate conditions

The media type is optional and defaults to all. Most queries leave it out:

CSS
@media (width >= 48rem) {
  /* styles */
}

The one type you will use regularly is print, for print stylesheets:

CSS
@media print {
  nav,
  .site-footer {
    display: none;
  }

  a[href^="http"]::after {
    content: " (" attr(href) ")";
  }
}

Width queries

Viewport width is the most common condition. There are two ways to write it.

Classic syntax

CSS
@media (min-width: 40rem) {
  /* 40rem and wider */
}

@media (max-width: 39.99rem) {
  /* narrower than 40rem */
}

min-width means "at least". max-width means "at most", and it includes the value itself, which is why older code uses awkward values like 39.99rem to avoid both queries matching at exactly 40rem.

Range syntax

Media Queries Level 4 introduced range syntax with comparison operators. It is supported in all current major browsers:

CSS
@media (width >= 40rem) {
  /* 40rem and wider */
}

@media (width < 40rem) {
  /* narrower than 40rem, exactly 40rem is excluded */
}

@media (40rem <= width < 64rem) {
  /* from 40rem up to, but not including, 64rem */
}

Range syntax reads like maths and removes the gap problem. < and >= at the same value never overlap and never leave a gap.

Range syntax is the clearer choice for new code. If you must support very old browsers, min-width and max-width remain valid and work everywhere. Check the feature on web.dev's Baseline information if you are unsure about your audience.

Which units to use

Use rem or em for breakpoints rather than px. In media queries, both units are based on the browser's initial font size, which users can change in their settings. If someone increases their default font size, an rem-based breakpoint moves with it, and the layout switches to the narrower version when the text would otherwise be cramped.

At the default font size of 16 pixels, 48rem equals 768 pixels.

Setting font-size on html in your CSS does not change what rem means inside a media query. Media queries use the browser's initial value, not your stylesheet's.

Height and orientation

You can query height in the same way as width:

CSS
@media (height < 30rem) {
  .hero {
    min-height: auto;
    padding-block: 1rem;
  }
}

This helps on phones held sideways, where vertical space is short.

orientation is portrait when the viewport is at least as tall as it is wide, and landscape otherwise:

CSS
@media (orientation: landscape) {
  .gallery {
    grid-template-columns: repeat(2, 1fr);
  }
}

Note that orientation describes the viewport, not how the device is held. A desktop browser window is usually landscape. A narrow window dragged tall becomes portrait. Prefer width and height queries for layout decisions, and use orientation only when the shape itself matters.

aspect-ratio is also available, for example @media (aspect-ratio >= 16/9).

Combining conditions

OperatorExampleMatches when
and(width >= 40rem) and (orientation: landscape)Both are true
, (comma)(width < 30rem), (height < 30rem)Either is true
notnot printThe whole query is false
onlyonly screen and (width >= 40rem)Hides the query from very old browsers; not needed today
CSS
@media (width >= 40rem) and (height >= 40rem) {
  .sticky-sidebar {
    position: sticky;
    top: 1rem;
  }
}

The sidebar becomes sticky only when there is enough room in both directions. On a short landscape phone it stays in normal flow, so it never covers content.

Input type: hover and pointer

Width tells you nothing about how someone interacts. A large tablet has no mouse. A small laptop window has one. Interaction media features answer that question:

FeatureValuesDescribes
hoverhover, noneWhether the primary input can hover
pointerfine, coarse, noneHow precise the primary pointer is
any-hoverhover, noneWhether any available input can hover
any-pointerfine, coarse, noneThe most capable of all pointers
CSS
.card-actions {
  opacity: 1;
}

@media (hover: hover) and (pointer: fine) {
  .card-actions {
    opacity: 0;
    transition: opacity 0.2s;
  }

  .card:hover .card-actions,
  .card:focus-within .card-actions {
    opacity: 1;
  }
}

The base style keeps actions visible. They only hide until hover on devices with a precise, hovering pointer, and :focus-within keeps them reachable by keyboard.

CSS
@media (pointer: coarse) {
  .toolbar button {
    min-width: 2.75rem;
    min-height: 2.75rem;
  }
}

Never use these features as the only way to make something usable. Many devices have several inputs, such as a touch screen laptop, and the primary input is a guess.

User preference queries

These features reflect choices the user made in their operating system or browser. Respecting them is part of building a responsive site.

Dark mode: prefers-color-scheme

CSS
:root {
  color-scheme: light dark;
  --bg: #ffffff;
  --text: #1a1a1a;
  --link: #0b57d0;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #121212;
    --text: #e8e8e8;
    --link: #8ab4f8;
  }
}

body {
  background: var(--bg);
  color: var(--text);
}

a {
  color: var(--link);
}

Define colours as custom properties once, then change only the values in the query. color-scheme: light dark tells the browser that the page supports both, so form controls and scrollbars match the scheme.

Reduced motion: prefers-reduced-motion

Some people experience dizziness or nausea from motion on screen, and turn on a "reduce motion" setting. Respect it:

CSS
.panel {
  transition: transform 0.4s ease;
}

@media (prefers-reduced-motion: reduce) {
  .panel {
    transition: none;
  }

  html {
    scroll-behavior: auto;
  }
}

You can also write it the other way round, and only add motion when the user has no preference:

CSS
@media (prefers-reduced-motion: no-preference) {
  .hero-image {
    animation: fade-in 0.6s ease both;
  }
}

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

This is mobile-first thinking applied to motion: the base experience is calm, and animation is the enhancement.

Reduced motion does not mean no feedback. Keep instant state changes, colour changes and short opacity fades. Remove large movement, parallax, zooming and auto-playing animation.

Contrast and forced colours

CSS
@media (prefers-contrast: more) {
  .card {
    border: 2px solid currentColor;
  }
}

@media (forced-colors: active) {
  .icon-button {
    border: 1px solid ButtonText;
  }
}

prefers-contrast: more means the user asked for higher contrast. forced-colors: active means a mode such as Windows High Contrast is replacing your colours with a limited palette. In forced colours mode, backgrounds and box shadows may disappear, so add real borders where shape matters, using system colour keywords such as ButtonText and CanvasText.

Media queries outside CSS files

In HTML

The media attribute works on <link>, <source> and <style>:

HTML
<link rel="stylesheet" href="print.css" media="print">

The browser still downloads stylesheets whose media does not match, but at a lower priority so they do not block rendering. You will use media on <source> in the next lesson.

In JavaScript

window.matchMedia() runs the same queries from JavaScript:

JavaScript
const wide = window.matchMedia('(width >= 48rem)');

function update() {
  document.body.dataset.layout = wide.matches ? 'wide' : 'narrow';
}

wide.addEventListener('change', update);
update();

The change event fires only when the result flips, which is far cheaper than listening to every resize event. Lesson 12 uses this for responsive navigation.

Summary

  • A media query combines an optional media type, media features in parentheses and logical operators.
  • Range syntax such as (width >= 48rem) is clearer than min-width and avoids gaps between queries.
  • Use rem or em breakpoints so layouts respect the user's default font size.
  • hover and pointer describe input type; width does not.
  • Respect user preferences with prefers-color-scheme, prefers-reduced-motion, prefers-contrast and forced-colors.
  • matchMedia() brings the same queries to JavaScript, with a change event.

In the next lesson you will use media conditions to send the right image to each screen.