Type something to search...
CSS Media Queries Level 4 and 5: Range Syntax and User Preferences

CSS Media Queries Level 4 and 5: Range Syntax and User Preferences

For a long time, "media query" meant one thing: @media (min-width: 768px). Width was the only question most of us asked the browser. But the specs have kept moving, and Media Queries Level 4 and Level 5 turned media queries into a much richer way of asking about the user's environment: how they interact with the page, what their screen can display, and what they've told their operating system they prefer.

In this article, we'll cover the new range syntax, the logical operators, the interaction and display features, and the user preference queries, with notes on what's safe to ship today.

A Quick Refresher on the Anatomy

A media query is a condition attached to a block of CSS:

@media screen and (min-width: 40rem) {
  /* styles */
}

It has an optional media type (screen, print, or all) and one or more media features in parentheses. Level 4 deprecated most of the old media types, like tv, handheld, and projection, so in practice you'll only use screen, print, and all, and you can usually leave the type out entirely.

The Range Syntax

The biggest everyday improvement in Level 4 is range syntax. Instead of the min- and max- prefixes, you write comparisons with real operators.

/* Old */
@media (min-width: 40rem) {
}
@media (max-width: 39.99rem) {
}
@media (min-width: 40rem) and (max-width: 64rem) {
}

/* New */
@media (width >= 40rem) {
}
@media (width < 40rem) {
}
@media (40rem <= width <= 64rem) {
}

The supported operators are <, <=, >, >=, and =.

Why It's More Than Syntactic Sugar

The old prefixes were always inclusive. min-width: 40rem matches at exactly 40rem, and so does max-width: 40rem. If you had one set of styles for "small" and one for "large", both matched at the boundary pixel. The traditional fix was a fractional value like max-width: 39.99rem, which could still cause gaps on screens with fractional pixel widths, like some zoomed or high-density displays.

With range syntax, you can write boundaries that don't overlap or leave a gap:

@media (width < 40rem) {
  .nav {
    flex-direction: column;
  }
}

@media (width >= 40rem) {
  .nav {
    flex-direction: row;
  }
}

Every possible width matches exactly one of those.

Ranges Between Two Values

The two-sided form reads just like math notation:

@media (40rem <= width < 64rem) {
  .sidebar {
    display: none;
  }
}

Range syntax works for any "range" feature: width, height, aspect-ratio, resolution, and so on.

@media (aspect-ratio > 16 / 9) {
  /* ultra-wide screens */
}

@media (resolution >= 2dppx) {
  /* high-density displays */
}

Support

Range syntax is supported in all current versions of Chrome, Edge, Firefox, and Safari, and has been for a few years. Unless you need to support quite old browsers, you can use it freely. If you do, the old prefixed form is the fallback. Browsers that don't understand a query treat it as not matching, so a range query in an old browser simply never applies.

Logical Operators: not, and, or

Level 4 cleaned up how you combine conditions.

/* and: both must be true */
@media (width >= 40rem) and (orientation: landscape) {
}

/* or: either may be true (a comma works the same way) */
@media (width < 30rem) or (height < 30rem) {
}

/* not: negate a condition, now allowed on individual features */
@media not (hover: hover) {
}

/* grouping with parentheses */
@media (width >= 40rem) and ((hover: hover) or (pointer: fine)) {
}

Before Level 4, not could only negate an entire query, including its media type, which led to confusing results. Now you can apply it to a single feature in parentheses. The or keyword is a readable alternative to the comma list, and it can be nested inside grouped conditions where a comma can't.

Interaction Media Features

Screen width tells you nothing about how someone is using the page. A 1200px-wide tablet may have no mouse, and a narrow browser window on a laptop has a precise trackpad. Level 4 added features that describe input devices directly.

hover and pointer

  • hover: hover means the primary input can hover (a mouse or trackpad).
  • hover: none means it can't (most touchscreens).
  • pointer: fine means the primary input is precise (a mouse or stylus).
  • pointer: coarse means it's imprecise (a finger).
  • pointer: none means there's no pointing device (for example, some keyboard-only or TV setups).
/* Only show hover effects where hover actually exists */
@media (hover: hover) {
  .card:hover {
    box-shadow: 0 8px 24px rgb(15 23 42 / 0.15);
  }
}

/* Bigger tap targets for touch */
@media (pointer: coarse) {
  .toolbar button {
    min-width: 44px;
    min-height: 44px;
  }
}

any-hover and any-pointer

hover and pointer describe the primary input. any-hover and any-pointer check whether any available input matches. A touchscreen laptop might report pointer: coarse if the touchscreen is considered primary, but also any-pointer: fine because a trackpad is present.

@media (any-pointer: coarse) {
  /* at least one input is a finger: keep controls touch-friendly */
  .slider-thumb {
    width: 28px;
    height: 28px;
  }
}

A good rule of thumb is to use hover and pointer for enhancements like hover effects, and any-pointer: coarse for safety features like larger targets.

Display Quality Features

resolution

Level 4 standardized resolution with the dppx unit, replacing the old prefixed -webkit-min-device-pixel-ratio:

.logo {
  background-image: url("/images/logo.png");
}

@media (resolution >= 2dppx) {
  .logo {
    background-image: url("/images/logo@2x.png");
  }
}

For CSS backgrounds, image-set() is often a cleaner choice, and for <img> elements, use srcset.

update

update describes how quickly the output device can change what it shows. It's fast for normal screens, slow for e-ink readers, and none for print.

@media (update: slow) {
  * {
    animation: none !important;
    transition: none !important;
  }
}

color-gamut and dynamic-range

These let you target wide-color and HDR displays:

.brand-accent {
  color: rgb(236 72 153);
}

@media (color-gamut: p3) {
  .brand-accent {
    color: color(display-p3 0.95 0.25 0.6);
  }
}

@media (dynamic-range: high) {
  /* HDR-capable display */
}

Support for color-gamut is good across modern browsers. dynamic-range is newer, so treat it as a progressive enhancement.

User Preference Media Features

This is where Level 5 comes in, and it's the most important group for accessibility. These features reflect settings the user chose in their operating system or browser. Respecting them is how you honor choices people made for comfort, readability, or medical reasons.

prefers-color-scheme

The best known of the group. It's light or dark:

:root {
  --bg: #ffffff;
  --text: #0f172a;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0b1120;
    --text: #e2e8f0;
  }
}

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

Also declare color-scheme: light dark on :root so form controls and scrollbars match. Modern CSS also has the light-dark() function, which picks one of two values based on the used color scheme and can reduce the amount of media-query code you need:

:root {
  color-scheme: light dark;
}

body {
  background: light-dark(#ffffff, #0b1120);
  color: light-dark(#0f172a, #e2e8f0);
}

prefers-reduced-motion

reduce means the user has asked for less motion, often because animation can trigger vestibular disorders, nausea, or migraines. It's widely supported and should be part of every project:

@media (prefers-reduced-motion: reduce) {
  .carousel {
    scroll-behavior: auto;
  }

  .hero__title {
    animation: none;
  }
}

The goal is less motion, not zero visual feedback. Replacing a slide with a quick fade is often better than removing the transition entirely.

prefers-contrast

more, less, custom, or no-preference. Use it to strengthen borders, remove low-contrast decorative text, or turn translucent backgrounds solid:

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

  .muted {
    color: inherit;
  }

  .glass-panel {
    background: Canvas;
    backdrop-filter: none;
  }
}

forced-colors

forced-colors: active means the user is in a mode like Windows High Contrast (Contrast Themes), where the browser replaces your colors with a limited, user-chosen palette. Background images and box shadows may disappear. Use system color keywords so your UI survives:

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

  .toggle[aria-checked="true"] .toggle__thumb {
    background: Highlight;
  }

  /* keep the brand logo's own colors; use sparingly */
  .brand-logo {
    forced-color-adjust: none;
  }
}

The most common bug here is a custom checkbox or focus indicator drawn only with box-shadow or a background color, which vanishes in forced colors mode. A transparent outline or border becomes visible in forced colors, which makes it an easy safety net.

prefers-reduced-transparency

reduce means the user wants fewer translucent and blurred surfaces. Support is currently limited (Chromium-based browsers have it; check caniuse for others), so treat it as an enhancement:

@media (prefers-reduced-transparency: reduce) {
  .site-header {
    background: #ffffff;
    backdrop-filter: none;
  }
}

prefers-reduced-data

The spec defines prefers-reduced-data: reduce for users on metered or slow connections, which would be ideal for skipping web fonts or background video. As of this writing it isn't shipped by default in any major browser, so don't rely on it. If you use it, make sure the default experience is already reasonably lean.

Scripting and Display Mode

Two more features worth knowing:

/* JavaScript disabled or unavailable */
@media (scripting: none) {
  .js-only {
    display: none;
  }
}

/* Installed as a PWA without browser UI */
@media (display-mode: standalone) {
  .install-banner {
    display: none;
  }
}

scripting is supported in current major browsers. display-mode is widely supported for installed web apps.

Using Media Queries in JavaScript

Everything above works with window.matchMedia, which is handy for syncing JS behavior with user preferences:

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)");

function setupAnimations() {
  if (reduceMotion.matches) {
    document.documentElement.classList.add("reduce-motion");
  } else {
    document.documentElement.classList.remove("reduce-motion");
  }
}

setupAnimations();
reduceMotion.addEventListener("change", setupAnimations);

Listening for change matters, because users can toggle these settings while your page is open.

Feature-Testing a Media Query

If you're not sure whether a browser understands a feature, remember that an unknown media feature makes the query evaluate as false. That's usually a safe default, but it can also bite you when you write a not query. not (unknown-feature) is also false, not true. Design so that the enhanced styles live inside the query and the base styles work without it.

Best Practices

  • Prefer range syntax for new code. It's clearer and avoids boundary overlaps.
  • Don't infer input from width. Use hover and pointer for interaction decisions.
  • Treat preferences as requirements. prefers-reduced-motion, prefers-contrast, and forced-colors exist for accessibility, not decoration.
  • Test the modes. Chrome DevTools can emulate most preference features from the Rendering panel. Firefox and Safari have similar options, and your OS settings are the real test.
  • Keep newer features progressive. prefers-reduced-transparency, dynamic-range, and prefers-reduced-data should enhance, never gate, the core experience.

Conclusion

Media queries have grown from a width check into a way of understanding the person on the other side of the screen. Range syntax makes breakpoints readable and precise. Interaction features let you design for fingers and mice instead of guessing from screen size. User preference features let you respect choices about color, motion, contrast, and transparency that people have already made.

Try auditing one of your stylesheets: convert the min-width queries to range syntax, check whether hover effects are gated behind (hover: hover), and turn on reduced motion and forced colors to see what breaks. You'll probably find a few quick wins.

Tags :
Share :

Related Posts

A Complete Guide to CSS Container Queries

A Complete Guide to CSS Container Queries

For more than a decade, responsive design meant one thing: media queries. You asked the browser how wide the viewport was and adjusted your layout ac

Continue Reading
A Comprehensive Guide to Installing Next.js

A Comprehensive Guide to Installing Next.js

Next.js has emerged as a powerful framework for building React applications, offering features like server-side rendering, static site generation, an

Continue Reading
Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

Advanced CSS with clamp(), min(), and max(): Simplifying Dynamic Styling

CSS has evolved significantly, and modern tools like clamp(), min(), and max() are powerful game-changers in dynamic styling. If you’ve struggl

Continue Reading