Modern Media Queries: Beyond Browser Widths
Table of contents
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:
Code can become verbose and difficult to manage when defining complex sets of breakpoints.
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.It's not possible to use custom properties (CSS variables) in media queries --
@media (min-width: var(--my-breakpoint))
will not work.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 devicescreen
for devices with a screen, andprint
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
, orrec2020
).color-index
andmin-color-index
to test the number of colorsdynamic-range
to test the brightness, contrast ratio, and color depth (standard
orhigh
)video-dynamic-range
to test a combination of brightness, contrast ratio, and color depth. It can bestandard
for most visual devices orhigh
for top-end displays.grid
to test whether the device is a grid-based screen such as a text-only terminal.scan
to test forprogressive
rendering as used on modern devices orinterlace
rendering as used by older CRT monitors and televisions.update
to test for the re-rendering frequency. It can benone
when a layout cannot update (a printed page),slow
(typically an e-ink display), orfast
(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 benone
(overflowing content hidden), orscroll
(you can scroll to overflowing content).overflow-block
similar, but for the block axis (left to right in western cultures). As well asnone
andscroll
, it can return printer-based values ofpaged
(content splits into pages), oroptional-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, andcourse
for less accurate devices such as a touch screen.any-pointer
is similar, but tests whether any available input device supportsnone
,fine
, orcourse
.
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 availableenabled
- scripting is supported and enabled, orinitial-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 windowfullscreen
- 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 controlsstandalone
- the app is running in its own window with no navigation controlswindow-controls-overlay
- the app is running in its own window with no navigation controls and the Window Controls Overlay API is controlling the title barpicture-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
andmax-height
for improved layout optionsscreen
andprint
for printer-friendly designsmonochrome
andcolor
for print and e-ink outputhover
andpointer
for standard and touch screen inputprefers-color-scheme
andprefers-reduced-motion
for accessibilityscripting
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.
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.