Modern Media Queries: Beyond Browser Widths

Craig BucklerCraig Buckler
14 min read

CSS3 Media Queries are the power behind Responsive Web Design. Rather than building separate sites to target mobile, desktop, and other devices, developers could create a single site which responded to the dimensions of the viewport. Layout techniques have evolved and media queries are no longer essential, but this tutorial discusses lesser-known options you could adopt to make your site more resilient and more usable by more people on a wider range of devices.

What are Media Queries?

You can create responsive designs in a variety of ways, but I favor a mobile-first approach. A simple single-column layout is the default - perhaps with a little padding to stop it touching the edge of the viewport:

main {
  margin-inline: 10px;
}

This works on all devices including old browsers which do not support media queries. However, a wide viewport creates long text blocks that are difficult to read. We can introduce a media query to detect when the viewport is equal or greater than 620px and set the content to a 600px centered column:

@media (min-width: 620px) {

  main {
    width: 600px;
    margin-inline: auto;
  }

}

You can style any elements within the media query block. For example, you could switch to a multi-column layout, reposition elements, change colors, use a different logo, etc.

Media Query <link> Syntax

The example above presumes you're using a stylesheet loaded by all devices. An alternative option is to split your stylesheets into device-specific styles and load them in an HTML <link> tag using a media attribute:

<link rel="stylesheet" media="all" href="core.css" />
<link rel="stylesheet" media="(min-width: 620px)" href="wide.css" />

All browsers load core.css. The wide.css stylesheet is only loaded when a device has a viewport width of at least 620px (there's no need to specify the @media block within it). This can have a performance benefit on sites with large stylesheets because devices can download the styles they require and no others.

Mixing Media Queries

As well as viewport dimensions, you can use different media types and features in expressions as described below. For example screen, print, and all target types of device.

You can combine media queries for more specific styling using and, or, and not. For example, target a screen viewport width between 600px and 1000px:

@media screen and (min-width: 600px) and (max-width: 1000px) {
  /* ... */
}

Target any device which is not a printer (a screen!):

@media not print {
  /* ... */
}

Target any device that has a monochrome display or a viewport width of 600px or less:

@media monochrome or (max-width: 600px) {
  /* ... */
}

Modern Range Syntax

The min-width, max-width, min-height, and max-height viewport conditions can be confusing and it's easy to get them the wrong way around. Most modern browsers support a newer range syntax, so:

@media (min-width: 600px) and (max-width: 1000px) {
  /* ... */
}

becomes:

@media (600px <= width <= 1000px) {
  /* ... */
}

Programmatic Media Queries

You can use media queries to programmatically react to device types, features, or events in JavaScript. You're unlikely to need the matchMedia API often, but it may be useful for advanced functionality.

The following code detects whether the screen has width of 600px or more. It checks the state on page load and triggers an event when it changes -- perhaps when a user rotates their screen or resizes their browser:

if (matchMedia) {

  // CSS media query
  const mq = window.matchMedia("(min-width: 600px)");

  // device state change event
  mq.addListener(mqChange);

  // detect initial state
  mqChange(mq);
}

// media query state handler
function mqChange(mq) {

  if (mq.matches) {
    // viewport width is >= 600px
  }
  else {
    // viewport width is < 600px
  }

}

The Downsides of Media Queries

Media queries raise some technical difficulties:

  1. Code can become verbose and difficult to manage when defining complex sets of breakpoints.

  2. Users can zoom or change font size. Browsers handle this differently, so it's best to use px viewport dimensions for consistency. That can be limiting.

  3. It's not possible to use custom properties (CSS variables) in media queries -- @media (min-width: var(--my-breakpoint)) will not work.

  4. Media queries have become less necessary as CSS has evolved.

Consider our example above which prevents content widths exceeding 600px:

main {
  margin-inline: 10px;
}

@media (min-width: 620px) {

  main {
    width: 600px;
    margin-inline: auto;
  }

}

We can replicate the layout in less code without any media query:

main {
  width: calc(100% - 20px);
  max-width: 600px;
  margin-inline: auto;
}

or even:

main {
  max-width: min(100% - 20px, 600px);
  margin-inline: auto;
}

We can improve it further to use editable custom properties that set margins proportional to the viewport and text widths proportional to the font size -- which is also determined by the viewport width!

:root {
  --margin: 2vw;    /* 2% of viewport width */
  --maxwidth: 75ch; /* 75 characters (approx) */
}

body {
  font-size: clamp(1rem, 0.5rem + 1vw, 3rem);
}

main {
  max-width: min((100% - 2 * var(--margin)), var(--maxwidth));
  margin-inline: auto;
}

Media query dimensions are also viewport based, but you often care more about the width or height of the containing element. Most browsers have supported container queries since 2022. They use a similar syntax to media queries; for example apply padding of 2em to a child element when its parent container has a width of at least 20em:

.container {
  container: outer / inline-size;
}

.child { padding: 1em; }

@container outer (min-width: 20em) {

  .child { padding: 2em; }

}

These techniques are beyond the scope of this article but, taking them to their logical extreme, it becomes less necessary to define viewport-based media queries or fixed breakpoints. Adaptive layouts define constraints so content can expand or contract to fill the available space. You can achieve this without media queries using modern CSS features such as flexbox, grid, columns, intrinsic sizes, clamp(), min(), max(), fit-content, auto-fit, minmax, gap, and more.

Modern Media Query Types and Features

Does this mean media queries are useless? Absolutely not!

Media queries support almost 30 device types and features collated into general types below. Some are for niche situations, but others offer greater opportunities for your web site or app.

Note that browser support is good unless indicated otherwise.

Device Type

You can target specific devices by their general type or display capabilities. These are most often used to provide printer-friendly web pages but you can also consider targeting ebook readers or even text-based terminals!

The most basic tests are:

  • all for any device
  • screen for devices with a screen, and
  • print for printers and print preview mode

monochrome determines whether the screen is monochrome such as those found on e-ink displays and ebook readers.

color tests the number of bits per color component (red, green, blue). Most color devices provide 8 bits per channel -- 24 bits in total -- but older devices may have limitations.

Examples:

/* default black */
body { color: black; }

/* any monochrome screen */
@media screen and (monochrome) {
  body { color: #222; }
}

/* any color screen */
@media screen and (color) {
  body { color: #123; }
}

/* color screen with at least 8 bits for R, G and B (24-bit color) */
@media screen and (min-color: 8) {
  body { color: #123456; }
}

/* color printer */
@media print and (color) {
  body { color: #123456; }
}

/* black and white printer */
@media print and (monochrome) {
  body { color: #222; }
}

resolution, min-resolution, and max-resolution test the screen's pixel density. Most recent phones and tablets can have high native resolutions, known as HiDPI or Retina displays. Each pixel may be invisible to the naked eye, so the browser implements a CSS resolution such as 360 x 760px. If the native resolution is 1440 x 3040px, the screen has a display density of 4x. In other words, a single CSS pixel uses 4x4 (16) physical pixels.

You may have used responsive images in HTML <img> tags, e.g. use img100.jpg on standard displays but replace it with img200.jpg on a high-density display of at least 2x (4 pixels per CSS pixel):

<img width="100" height="100"
  alt="responsive image"
  src="img100.jpg"
  srcset="img100.jpg 1x,img200.jpg 2x" />

A similar option is available in CSS using the image-set() property:

.myimage {
  background-image: image-set("img100.jpg" 1x, "img200.jpg" 2x);
}

but you can use a media query if you prefer:

.myimage {
  background-image: url("img100.jpg");
}

@media (min-resolution: 2x) {
  .myimage { background-image: url("img200.jpg"); }
}

More advanced but less-used device testing options include:

  • color-gamut to test the approximate range of color (srgb, p3, or rec2020).

  • color-index and min-color-index to test the number of colors

  • dynamic-range to test the brightness, contrast ratio, and color depth (standard or high)

  • video-dynamic-range to test a combination of brightness, contrast ratio, and color depth. It can be standard for most visual devices or high for top-end displays.

  • grid to test whether the device is a grid-based screen such as a text-only terminal.

  • scan to test for progressive rendering as used on modern devices or interlace rendering as used by older CRT monitors and televisions.

  • update to test for the re-rendering frequency. It can be none when a layout cannot update (a printed page), slow (typically an e-ink display), or fast (most phones, tablets, and monitors).

Layout

Media queries are most often used for responsive designs which test the device width as shown above. You can use width to test for an exact dimension:

@media (width: 360px) {
  /* ... */
}

More realistically, you'll use min-width, max-width, or operators to test for a range:

@media (600px <= width <= 1000px) {
  /* ... */
}

Fewer sites consider height, min-height, and max-height but they have uses. In this example, the viewport height determines the site <header> layout:

  • when the viewport height is less than 1000px, the header scrolls up and out of the viewport as you scroll the content
  • when the viewport height is 1000px or more, the header becomes larger and fixes itself to the top of the viewport so it's always available no matter where you scroll.
/* smaller device default */
header {
  width: 100%;
  height: 8vh;
}

/* taller device */
@media (1000px < height) {

  /* adjust header */
  header {
    position: fixed;
    height: 16vh;
    top: 0;
    left: 0;
  }

  /* push content below fixed header */
  main {
    padding-top: 18vh;
    scroll-margin-block-start: 18vh;
  }
}

(Note: the vh unit is 1% of the viewport's height. The header height will increase accordingly, so you could consider using clamp() to specify minimum and maximum dimensions.)

orientation tests whether the device is in portrait or landscape view. It's probably not something you'll use often because width and height are more critical, but you could use it for to change the logical direction of content, e.g.

/* default portrait view */
main {
  display: flex;
  flex-direction: column;
}

/* landscape view */
@media (orientation: landscape) {
  main { flex-direction: row; }
}

aspect-ratio, min-aspect-ratio, and max-aspect-ratio provide more flexibility because you can specify how the width compares with the height. It can be complex, but the following media query triggers when the device is in landscape mode - even when the width is just 1px more than the height:

@media (min-aspect-ratio: 1/1) {
  /* landscape */
}

Newer, less-used layout options include:

  • overflow-inline determines how the device handles overflows along the inline axis (top to bottom in western cultures). Values can be none (overflowing content hidden), or scroll (you can scroll to overflowing content).

  • overflow-block similar, but for the block axis (left to right in western cultures). As well as none and scroll, it can return printer-based values of paged (content splits into pages), or optional-paged (page breaks can be manaully triggered).

Pointer Input

A device can have any number of pointing devices: mouse, trackpad, trackball, pen, touch screen, etc. The browser makes an assumption about which is the primary input type. The following media queries are available:

  • hover tests whether the user's primary input device can hover over an element (hover) or not (none). Generally, you can hover with a mouse or track pad, but not with a touch screen or pen.

  • any-hover is similar, but tests whether any available input device could hover.

  • pointer tests the accuracy of a pointing device: none for no primary pointing device, fine for accurate devices such as a mouse, and course for less accurate devices such as a touch screen.

  • any-pointer is similar, but tests whether any available input device supports none, fine, or course.

It can be difficult to activate a button or a link on a touch screen if it's less than 7mm width and/or height. You could test for touch screen devices and alter the click area accordingly:

.closebutton {
  width: 20px;
  height: 20px;
  padding: 0;
}

@media (hover: none) or (pointer: coarse) {
  .closebutton { padding: min(0px, 7mm - 20px); }
}

That said, it's better to ensure your interactive elements are reasonably large on all devices. Not every mouse or pen user has good motor abilities to click small controls.

Accessibility and User Preferences

Sites often provide light (black on white) and dark (white on black) theming options. CSS can determine the OS theme preference using prefers-color-scheme:

/* default light theme */
body {
  color: black;
  background-color: white;
}

/* dark theme */
@media (prefers-color-scheme: dark) {
  body {
    color: white;
    background-color: black;
  }
}

It's often more practical to define a range of color custom properties so they're available throughout your stylesheets. You may also want to offer an override switch so the user can choose a specific theme irrespective of their OS choice.

You should also consider the new color-scheme property which can enable rudimentary theming in one line of CSS without media queries!

:root { color-scheme: light dark; }

prefers-reduced-motion tests whether the user chosen to minimize non-essential animations in their OS. These effects can be problematic for those with vestibular motion disorders, so you can test for reduce or no-preference. The following snippet overrides all transitions, animations, and scroll behaviours to make effects occur instantly:

@media (prefers-reduced-motion: reduce) {

  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

}

A similar media query is prefers-contrast which tests the user's contrast preferences. Values can be no-preference, more, less, or custom (the user has specified a limited color palette). You could combine this with prefers-color-scheme to specify color sets or use the new color-mix() function to automatically generate color sets.

More advanced but less-used accessibility options include:

  • forced-colors to test for limited color palettes such as Windows High Contast Mode (active)

  • inverted-colors to test if the OS has inverted the colors

Scripting

scripting tests whether JavaScript is available. The values can be:

  • none - scripting is not available
  • enabled - scripting is supported and enabled, or
  • initial-only - scripting is enabled during the initial page load, but not afterwards

In the past, developers would often add a js-enabled or similar class to the <html> element using JavaScript when it first runs:

<script type="module">
document.documentElement.classList.add('js-enabled');
</script>

This allowed you to alter or hide JS-powered widgets until scripting activated, e.g.

.widget { display: none; }
.js-enabled .widget { display: block; }

This is no longer necessary and you can check in CSS alone:

.widget { display: none; }

@media (scripting: enabled) {
  .widget { display: block; }
}

You could choose to show a loading spinner or a skeleton layout before any JavaScript loads and runs.

Note that Web Components can use the :defined pseudo class to hide any element that has not initialized. You could use this in conjunction with a media query, e.g.

/* hide component by default */
my-component:not(:defined) {
  display: none;
}

@media not (scripting: none) {

  /* default styles */
  my-component {
    width: 10em;
    height: 20em;
  }

  /* display component skeleton before initialization */
  my-component:not(:defined) {
    display: block;
    background-color: #ddd;
  }

  /* component initialized */
  my-component:defined {
    display: block;
    background-color: #9ff;
  }

}

Screen Modes

display-mode tests whether a web site or application is running in a normal browser tab or an alternative way. This is most used in Progressive Web Apps and the values can be:

  • browser - the app is running in a normal browser tab or window
  • fullscreen - the app is running full screen via the Fullscreen API or the PWA manifest (note that pressing F11 or similar will not necessarily trigger this value).
  • minimal-ui - the app is running in its own window with minimal navigation controls
  • standalone - the app is running in its own window with no navigation controls
  • window-controls-overlay - the app is running in its own window with no navigation controls and the Window Controls Overlay API is controlling the title bar
  • picture-in-picture - the app is displayed in a floating, always-on-top window triggered by the Picture-in-Picture API.

Performance

prefers-reduced-data tests whether the user has requested content which consumes less bandwidth -- perhaps because they are on a slower, more limited, or expensive network. Support is limited to Chrome-based browsers and it must be specifically enabled. The values can be no-preference or reduce, e.g.

body {
  background-image: url("low-res.jpg");
}

@media (prefers-reduced-data: no-preference) {
  body {
    background-image: url("high-res.jpg");
  }
}

Note this would cause low-res.jpg to load by default on non-supporting browsers. That may not be your preferred choice.

Summary

You're unlikely to use all the media query options above, but there's no reason to limit yourself to min-width and max-width. I recommend you consider:

  • min-height and max-height for improved layout options
  • screen and print for printer-friendly designs
  • monochrome and color for print and e-ink output
  • hover and pointer for standard and touch screen input
  • prefers-color-scheme and prefers-reduced-motion for accessibility
  • scripting for JavaScript and no-script support
  • container queries

CSS media queries are no longer essential for responsive web design, but they remain a viable option for adding supplemental functionality, better resilience, and improved usability on a wider range of devices. They allow your web sites and apps to reach wider audiences.

0
Subscribe to my newsletter

Read articles from Craig Buckler directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Craig Buckler
Craig Buckler

I am a freelance UK full-stack web developer with a few decades experience.