Skip to contentSkip to footer

If you want to be kept up to date with new articles, CSS resources and tools, join our newsletter.

Media queries are what make modern responsive design possible. With them you can set different styling based on things like a users screen size, device capabilities or user preferences. But how do they work, which ones are there and which ones should you use? Here's the complete guide to media queries.

What this guide will go through:

What are media queries?

What is a media query? A media query is a specific feature of CSS that lets you conditionally apply styling based on a media type, a media feature or both. You use them primarily to check the screen dimensions and apply CSS based on that, but media queries can do many other powerful things.

How a media query is structured

Here's the general structure of a media query:

@media <media-type> and (media feature) {
  /*...*/
}

It begins with the @media keyword, also called an "at-rule", optionally followed by a media type and zero or more media features.

A real example of a media query is:

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

In English, what this says is this: "if the site is being shown on a screen and that screen's width is at least 400px wide, apply this CSS".

Both the media type and the media feature are optional, so this is a valid media rule:

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

And this is a valid media query too:

@media (min-width: 400px) {
  /*...*/
}

If you leave out the media type, it will be seen as the media type all, which is usually fine.

Logical operators

Next to media type and media features, we also have a few "logical operators" that go inbetween those parts: and, or and not. Here's how those work:

The and operator

You can use the "and" operator not just between a type and a feature, but you can also use it to "chain" multiple media features using and, like so:

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

Which tells a browser to apply this CSS if the browser width is between 400 and 800 pixels.

The or operator: , (a comma)

You can use a comma to do "or", like so:

@media screen, print {
  /*...*/
}

which applies to screen or print. Each comma separated line is its own media query and as soon as one of them evaluates to true, the CSS in the media query is applied. In the future you'll also be able to use or instead of a comma.

The not operator

Lastly, there is not, which can be used to negate an entire media query. So the following query will be applied everywhere except print:

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

If you use the not operator you must set a media type (like screen or print) as well. If you don't, the media type will be all, and then your media query will read "not all" so it won't get applied anywhere.

not inverts the entire media query. Scroll down to not() notation for an upcoming feature that will allow you to negate just a part of a media query.

There's also the only logical operator, but in modern browsers you don't need to use this, so we'll pretend it doesn't exist.

Nesting

You can nest media queries in other media queries and that will work fine! So instead of

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

you can also write

@media (min-width: 400px) {
  @media (max-width: 800px) {
    /*...*/
  }
}

In this example it might not be so useful, but you could wrap your entire CSS in an @media screen {} and do your responsive design with nested media queries, and then have a fully custom print stylesheet. With this, that'll look a little cleaner.

Imports

You can optionally append one or more comma-separated media queries to the import statement to conditionally import the file, for example like so:

@import 'print.css' print;

@import 'dark.css' screen and (prefers-color-scheme: dark);

If a media query does not match, the CSS won't be applied, but the file will still be downloaded.

Media query in HTML

You can also use a media query in your HTML, for example in a <style> or <link> tag, like so:

<link href="style.css" rel="stylesheet" media="screen and (min-width: 400px)" />

<style media="print">
  /* ... */
</style>

It's worth noting that any linked files will still be downloaded by your browser even if they doesn't apply.

You can also use media queries to control which responsive image to load using the img element with a sizes attribute, or a picture element with different source elements that each have a media attribute, but since that's a large topic and does not concern CSS, we'll leave that for another guide.

Media types

The media type is used to describe the type of device that a browser is running on. There used to be loads, but the level 4 specification deprecated a whole lot that were never implemented anyway, leaving us with 3 we should care about:

  • all
  • screen
  • print

All

As mentioned earlier, if you don't specify a media type, it will default to "all", which means the css will apply to all devices.

Screen

Probably what you're reading this article on now!

Print

For when you print a page, or any "paged" media (Like a book! Did you know people use HTML and CSS to mark up books?)

Did you know you can easily switch between screen and print stylesheets in Polypane with our emulation features?

So just for completeness, here's all the other ones that you never got to use, and probably never will:

  • aural (which was replaced by speech),
  • speech (...also deprecated. Originally for use with speech synthesizers, like screen readers. Never implemented in browsers.)
  • tty (for the terminal),
  • tv (... like a screen, but different...somehow?),
  • projection (projectors),
  • handheld (for phones, they were actually used for a while before media expressions were a thing),
  • braille (for braille devices) and
  • embossed (like a combination of print and braille, so for printed braille).

Using media queries

When using media queries it's good to give yourself a few rules, so you don't randomly add media queries and end up with CSS that behaves unpredictably and is hard to maintain.

When starting fresh, I recommend to write CSS from narrow (mobile) to wide (desktop) and then only using min-width for your styling. That way you're always designing "up" and your CSS remains easy to reason about: All the CSS you write will be additive compared to the base styling. Your original CSS might place things in a column:

main {
  display: flex;
  flex-direction: column;
}

and when you get wide enough to show two columns, you switch to horizontal:

@media (min-width: 40rem) {
  main {
    flex-direction: row;
  }
}

Because the main element already has display: flex;, all we needed to do at the wider breakpoint is change the flex-direction. We prevent code duplication and everything is more readable and faster to download.

We can extend this reasoning to all the media features described below. Write your CSS for the starting point: the most well-supported, the smallest or the most accessible version. Then progressively add more styling using media queries.

For more in-depth tips on how to develop a responsive website, read our article Responsive design ground rules.

Using media query features

Media query feature make up the main part of a media query and have the most influence on what you're designing. Media feature do a lot. They let you check for device and browser capabilities, settings and user preferences beyond just the type of device.

Dimensions

By far the most used media features are related to browser dimensions. Here are they:

Width and Height

You can check for exact width, min-width and max-width, and exact height, min-height and max-height.

width and height you'll probably never want to use, they only apply at that exact width.

More useful are the min-* and max-* values, which let you write CSS for screens starting at a certain size and larger (using min-*), or up to a certain size (using max-*).

All the width and height media features support the regular CSS units like pixels and ems. It's recommended to use the em unit for media queries, so it scales nicely when people zoom in their browser. This won't happen with pixels, and rem is the same size as an em when it's used in media queries. rem also has some bugs in Safari. So, em's are best.

If you think sizing in ems is hard because you don't know how large an em is: media queries are handled at the top level, so 1 em is always 16 pixels or what the browser set as the default font size (and so are rems).

Did you know Polypane automatically detects the width and height media queries in a site and creates panes for them? That way, you're always building sites to your own specifications. It also supports sizing panes in ems so you don't even have to calculate between pixels and ems!

Aspect ratio

You can also test for the relation between width and height, with the aspect-ratio and orientation media features.

The aspect-ratio media feature takes a fraction, and also has the more useful min-aspect-ratio and max-aspect-ratio media features. It will lets you check if a browser has a certain ratio between width and height. The way to think about it is: the first number represents the width, the second number represents the height. This means an aspect ratio of 2/1 is twice as wide as it is high.

You can test for (only) square screens:

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

Or check for screens that are 16 by 9:

@media (aspect-ratio: 16/9) {
  /*...*/
}

Like width and height, you'd usually use the min-* and max-*variants. With a certain aspect ratio you can decide to show images in a portrait or landscape mode, for example.

With min-aspect-ratio, you can check for screens that are wider than they are high:

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

Conversely, by using to max-aspect-ratio: 1/1, you switch that around to only screens that are higher than they are wide.

Instead of doing that though, you can also just use orientation: landscape and orientation: portrait, which mean the same thing and are a little clearer.

Prefers-color-scheme

With prefers-color-scheme you can check if a user prefers to see a dark mode or light mode version of your website.

Some users might prefer dark mode because it's easier on their eyes, because their environment is dark or because they're sensitive to bright lights. Conversely, users might prefer your light mode for the usually increased contrast or because they visit your site in a bright space.

Here's how to test for both:

@media (prefers-color-scheme: dark) {
  /* wants dark mode */
}

@media (prefers-color-scheme: light) {
  /* wants light mode */
}

Between, these, light is considered to be the default. So much so, that a third option no-preference was recently removed from the specifications due to lack of implementations.

Did you know Polypane's emulation mode makes it really easy to test both a dark and light theme side-by-side.

Implementing a dark mode

If you already have a website and want to add dark mode, the neatest way to go about it is to redesign each part. This will give you the most control and highest quality implementation.

For many sites, it might be hard to go through all parts of a site, or there's no time or budget available. For those sites, there's a thing I like to call "cheap dark mode", and it looks like this:

@media (prefers-color-scheme: dark) {
  :root {
    background: #111;
    filter: invert(1) hue-rotate(180deg);
  }

  img,
  video {
    filter: invert(1) hue-rotate(180deg);
  }
}

Here's how it works: First we set a dark background on the :root, which is basically the html element. This doesn't have to be #111 (very dark gray), it can also be pure black (#000) or you can add a little color to it. That all depends on your brand and design, so pick one that works for you.

The next line is where the magic happens. With filter we can invert all the colors. This will make light dark and dark light. It won't affect the background which is why we defined that ourselves.

Invert also has another effect though, it also inverts colors. So blue will become orange, green becomes pink and so on. That's not great, because it means you just lost your brand colors. Fear not though, we can get them back by rotating the hue back to the original colors with hue-rotate(180deg), undoing the invert specifically for the hue of your colors.

At this point, all images and videos on your site look super weird, so what we need to do there is double-apply the invert and hue-rotate, turning both back to their original colors. Now you have a site in dark mode, where all images and videos are shown as they are. Not bad for just a few lines of CSS!

Prefers-reduced-motion

With prefers-reduced-motion, users can indicate they want to see less stuff happening on screen. The reason they want to do this can be things like motion sickness, vestibular disorders or they just plainly don't want to wait for your nice animations to finish.

@media (prefers-reduced-motion: reduce) {
  /* wants reduced motion */
}

@media (prefers-reduced-motion: no-preference) {
  /* doesn't want reduced motion */
}

If a users has prefers-reduced-motion turned on it doesn't mean they want no motion, but you have to be mindful: use motion only where it helps understanding and if you do, keep the motion small (so fade instead of swoosh). Otherwise just turn them off. And for video's, make sure you don't auto-play them.

Implementing reduced motion

If you're starting a new project and want to incorporate support for reduced motion it's a good idea to consider motion an enhancement: have no or little motion as the default, and only when refers-reduced-motion: no-preference is set, add additional motion to your website. That way, the default experience is the more accessible one.

For an existing site, this might be a lot of work, so we also have a cheap reduced motion script:

@media (prefers-reduced-motion: reduce) {
  ::root {
    view-transition-name: none !important;
  }
  body *,
  body *::before,
  body *::after {
    animation-delay: -1ms !important;
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    transition-delay: -1ms !important;
    scroll-behavior: auto !important;
    background-attachment: initial !important;
    view-transition-name: none !important;
  }
}

This will turn off all animation for all elements so it's as relatively blunt method, but it's perfect to retroactively add to a site to make it more accessible. The animation is set to 1ms so the animationend and transitionend events are still fired because your javascript might depend on them, but with the negative delay you won't notice the difference.

Did you know Polypane makes it really easy to test your prefers-reduced-motion implementation side-by-side with a regular pane.

Prefers-contrast

Prefers contrast indicates whether a users prefers more or less contrast in their interface. For example visual impairments can make it difficult to make out details or subtle differences in color so people that have that will prefer higher contrast. On the other hand, other people might be sensitive to harsh high contrast and prefer colors to be closer to each other.

@media (prefers-contrast: more) {
}

@media (prefers-contrast: no-preference) {
}

@media (prefers-contrast: less) {
}

@media (prefers-contrast: custom) {
}

prefers-contrast: custom is an odd one out. If you dont prefer more, less or the same (no-preference) contrast, why does this exist? The custom value will match when forced-colors is active but the user doesn't have a high or low constrast theme selected, but something in the middle. This way, @media (prefers-contrast) without a value will still match and that will let you re-use the media query to reduce visual complexity.

The "increase contrast" option in the macOS accessibility menu will trigger prefers-contrast: more:

The macOS settings with increase contrast turned on

When you turn it on, your entire operating system also switches to an increased contrast mode which you can use for inspiration on your site. As you can see from the screenshot, macOS doesn't turn everything full black on full white (though it does bump up the text contrast), but instead it increases the contrast between sections, by adding clear borders instead of soft shadows and suble background tints.

On Windows, prefers-contrast is turned on when you turn on forced-colors and will match more, less or custom depending on the contrast ratio between the background and default text color.

Did you know Polypane makes it really easy to test your prefers-contrast ands forced-colors implementation side-by-side with regular panes.

Color-gamut

Colors on the web so far have been limited to just the sRGB color gamut, but modern screens are able to display much more colors, like the p3 color space. With the color-gamut you can test if a device has such a wider color space.

There are three potential options, and if it supports the larger color space, it automatically includes support for the smaller color space.

  • srgb This is the one we all know :)
  • p3 This is what for example modern iPhones use, often called "wide gamut"
  • rec2020, this is the largest color space available right now, but you basically won't find this in the wild.

If a user has a display with a wider gamut, you can use images in that color space and they'll be more vibrant than ones in sRGB.

Not supported yet, but coming soon are new CSS notations you can use to describe colors in these wider color space (since this is not possible with rgb, hsl or hex, which are limited to sRGB).

For these new colors, there are three new css functions: lab(), lch() and color(). For now, only Safari supports the color() function, and the other two are supported nowhere.

Lab and lch allow you to describe more precise colors than rgb and hsl, but with color() you can explicitly opt-in to a color gamut, like so:

p {
  /* the reddest red that sRGB has */
  color: rgb(255 0 0);
}
@media (color-gamut: p3) {
  p {
    color: color(display-p3 1 0 0); /* The reddest red the display can show */
  }
}

The display-p3 is the only value supported for now, and the three numbers are floating point numbers for the R, G and B channel.

Display-mode

If you're designing a game or video player, you might have more than just the game/video on the page but when someone switches to fullscreen you only want to show the game/video. This media query lets you do that. display-mode is supported by all modern evergreen browsers, and will let you test for how your site is shown with four possible options:

  • browser
    This is the default mode, where your page is shown in a regular browser window.

  • fullscreen
    Your page is displayed full screen and there is no browser chrome visible. If there is no fullscreen media query defined, your browser will instead apply styles defined in a standalone media query.

  • standalone
    The page is not shown fullscreen, but also doesn't have all of the regular browser chrome, Instead it looks like a standalone (desktop) application. If there is no standalone media query defined, your browser will instead apply styles defined in a minimal-ui media query.

  • minimal-ui
    Your page is shown in its own window, but the browser will still show some chrome for controlling navigation (like popup windows). If there is no minimal-ui media query defined, your browser will instead apply styles defined in a browser media query.

As you can see, most of these values fall back to the next one, so if you define a minimal-ui style, that will also be applied to standalone and fullscreen.

Resolution/-webkit-device-pixel-ratio

With the resolution media query you can test for a displays pixel density. There is resolution, which tests for a single specific pixel density and min-resolution and max-resolution which give a lower and upper bound.

You can use this to serve retina background images to displays that support it, for example.

Resolution takes a number with either a dpi (dots per inch), dpcm (dots per centimeter) or dppx (dots per pixel). In CSS, a pixel is always 96dpi, so 1dppx is a regular screen resolution, and 2dppx is 'retina'.

Instead of dppx you can also just use x, so your CSS could look like this:

@media (min-resolution: 1x) {
  /* your regular background here */
}

@media (min-resolution: 2x) {
  /* your retina background here */
}

Older versions of Safari (before TP Release 138) don't support the resolution media query. They have a similar feature called -webkit-device-pixel-ratio (as well as -webkit-min-device-pixel-ratio and -webkit-max-device-pixel-ratio), which accepts a number without a unit. The implied unit is the same as dppx though, so the following CSS has the same effect as the resolution sample above:

@media (-webkit-min-device-pixel-ratio: 1) {
  /* your regular background here */
}

@media (-webkit-min-device-pixel-ratio: 2) {
  /* your retina background here */
}

To improve support, all other modern browser also support the -webkit-device-pixel-ratio notation so between resolution and -webkit-device-pixel-ratio, you can safely use the latter for the widest support. Consider switching to resolution though, unless you also need to support older versions of Safari.

Overflow

Overflow can test how a device handles content that's larger than fits. It exists of two properties, overflow-block, for the block direction (usually top to bottom) and overflow-inline for the inline direction (usually left to right).

For overflow-block there are 4 potential values to check against:

  • none, meaning that anything that overflows is simply not displayed
  • scroll, you can scroll to content that overflows
  • optional-paged, you can scroll but page breaks can be manually triggered
  • paged, content that overflows is shown on a next page.

And overflow-inline only has two values:

  • none, meaning that anything that overflows is simply not displayed
  • scroll, you can scroll to content that overflows

Update

update is used to detect how often the media type can update. Possible values are:

  • none, meaning it can't update, like printed paper.
  • slow, where updating is slow like on e-book readers or low power devices.
  • fast where updating is not constraint by device capabilities, like regular screens.

Inverted colors (Safari only)

This media query indicates that the operating system has inverted all colors. The operating system in this instance is MacOs, the only operating system that supports this (and conversely, only Safari supports this media query). It's either off or on:

@media (inverted-colors: none) {
  /* colors are normal */
}

@media (inverted-colors: inverted) {
  /* colors are inverted */
}

You might think that you'd use this to double-invert your images and videos like we did with our cheap dark mode, but Safari already does this for you. You can however, still hue shift your entire site to make sure your brand colors are followed.

Dynamic-range

Some displays can display "HDR", or "high dynamic range", characterized by large contrast, brightness and color depth. The specification gives two options: high and standard. If it's high, this means that you can use display-p3 colors (currently only supported in Safari) to provide more vibrant colors for your visitors. To test for support for specific color spaces, you can use the color-gamut media feature

Interaction: pointer and hover

We have many more different input/pointing devices now compared to when the web got started. Mouse pointers still exist, but we also have touch, external controllers like Wii remotes and even things like AR hand detection.

Some things that are easy to do with a mouse are harder or impossible to do with touch devices, like hitting small targets or even hovering. With the interaction media features you can adapt to these devices in clever ways.

The way I would go about this is consider a touch device as the most minimal implementation. It won't have hover effects, and the precision of your input device is coarse (It's the size of your thumb).

@media (hover: none) and (pointer: coarse) {
  /* you're on a touch-only device */
}

For this group, you can't have things popping up on hover so they need to be visible or behind an explicit toggle, and your tap targets will need to be larger.

But those devices could also have support for a stylus, still not allowing hover, but making it easier to point to specific parts on the screen. While it's a good idea to have large enough clickable targets even if pointer: fine, you can for example place them closer and make more efficient use of the space.

@media (hover: none) and (pointer: fine) {
  /* you're on a device without hover but with a stylus
     or other fine pointing device */
}

Then as you start to get hover capabilities, you can add those with:

@media (hover: hover) and (pointer: coarse) {
  /* you're on a device with hover but a coarse pointer*/
}

Devices that allow for hovering but have a coarse pointer are things like Wii controllers and Kinect. They let you point at things but without great precision. You'll want large enough targets and you can add hover effects as an indication of where they're pointing.

Lastly, we end at what many will think of as "normal", but is actually the most well-featured situation: a device with a mouse/trackpad.

@media (hover: hover) and (pointer: fine) {
  /* you're on a device with a mouse or trackpad */
}

These devices can hover over elements and precisely target them. Users of these devices might still have different capabilities (Not just permanent! They also might be tired, or have greasy fingers) so design accordingly.

Though iOS 13.4+ support these media features, it will always match pointer:coarse and hover:none (and their any- counterparts), even when using the new trackpad or the pencil.

Multiple pointing devices

The pointer and hover media queries give you information about the primary pointing device. But what about the touchscreen-with-stylus example where you have a coarse and a fine pointing device simultaneously? Even if the user has a stylus, the primary pointing device is still a touch screen, so coarse. For those situations you can use any-hover and any-pointer. This will test if any pointing device exists that matches the criteria.

Less interesting media queries

There are a few more media queries that are not as useful in daily usage but I didn't want to leave out.

Color

the color media query (and it's min-* and max-* variants) lets you detect if the screen your page is being shown on has any color, and if so, how much:

@media (color) {
  /* you're on a color screen */
}

You can also give it a value which translates to the number of bits per color component, so for each of red, green and blue separately. Most modern screens have 8 bits per channel, but 10 bit screens and even 12 bit screens are becoming more common. The value is not the total number of colors, but the bits per color, so don't confuse it with "8-bit color", which is something different. 8 bits per color channel corresponds to a 24-bit color screen.

@media (min-color: 8) {
  /* you're on a "full" color screen */
}

Monochrome

Somewhat the inverse, monochrome (and it's min-* and max-* variants) lets you detect if the screen is shown on a monochrome (like greyscale) media type. You can again use just the value to detect a monochrome media type:

@media (monochrome) {
  /* you're on a monochrome media type */
}

And like color, it also has an optional value that's the number of bits per pixel. If you test for monochrome: 0, that will check if the device is not monochrome (so it has colors). monochrome: 1 will detect a device with pixels that are either on or off (like e-paper).

An interesting use-case for monochrome is that you can use it to detect when a page is printed in color or in monochrome:

@media print and (monochrome: 0) {
  /* you're printing in color */
}
@media print and (monochrome) {
  /* you're printing in monochrome */
}

Grid

The grid media query will let you detect when a page is shown in text-only terminals or basic phones with fixed fonts. It's value is either a 0 or a 1, and unlike color or monochrome, you need to explicitly add it:

@media (grid: 0) {
  /* shown on a regular screen */
}
@media (grid: 1) {
  /* shown on a grid device  */
}

Upcoming media query features

While all the previous features have pretty good support across modern browsers, there are also many upcoming features that are only supported by a few browsers or that will hopefully be implemented soon.

Please note that some of these are still in a drafting phase, which means they might be changed or scrapped before becoming part of a specification. I'll try to keep this document updated!

Prefers-reduced-data (no support) Available for testing in Polypane

Not everyone is lucky enough to have fast, or reliable, or unlimited data plans. Browsers can send the Save-data: on header, and web servers can then choose to send smaller images and videos and disable any form of polling or preloading. Even though turning on the sending of that header is hidden deep in settings (on mobile) or requires a third-party plugin (on desktop), a large number of people still end up use this header.

Unfortunately dealing with this on a web server level is often hard to do, either because you lack access, or the configuration requirements to make it work are just too complex. That's unfortunate because it's potentially very impactful.

Coming up is a prefers-reduced-data: reduce media query, which will let you target this situation in CSS. Though you can do less with that compared to a save-data header (which you could use to send an entirely different website, basically), you can still use it to prevent downloading unneeded fonts and background images, or to request smaller background images.

You can emulate the prefers-reduced-data media query in Polypane using the emulation panel

While we wait on this feature to land, we can already use JavaScript instead to detect the save data preference or a slow connection using the Network Information API:

const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection || {};

if (connection.saveData || ['slow-2g', '2g', '3g'].includes(connection.effectiveType)) {
  /* data-saving measures like not preloading videos */
  preloadVideo = false;
}

The Network Information API is available in Chromium browsers and behind a flag in Firefox. It can tell if you saveData is on and gives you rough information on the type of connection. effectiveType takes into account not just the type (wifi, cellular etc) but also how long previous roundtrips to the server took and what the download speed is. There's slow-2g, 2g, 3g and 4g as possible values.

To learn more about how to design for prefers-reduced-data, read our article Creating websites with prefers-reduced-data

Prefers-reduced-transparency (Chromium 118, Supported behind a flag in Firefox)

User can use this to indicate they prefer to see things on solid colors, usually due to visual impairments making it hard to read text on, for example, background images. But it can also help people with for example dyslexia or concentration problems to read your content easier. Note it doesn't say no-transparency.

@media (prefers-reduced-transparency: reduce) {
}

@media (prefers-reduced-transparency: no-preference) {
}

Wanting less transparency is not the same as wanting more contrast and should not be lumped together. prefers-contrast and prefers-reduced-transparency are there for different reasons and can be compensated differently for.

Forced-colors (Support in Chromium browsers and Firefox) Available for testing in Polypane

Forced colors, or high contrast, will strip out all your background images if there's text over them and overwrite all your other colors, making everything uniform with the rest of the operating system. This helps people with visual impairments by making all text easier to read for them.

The Polypane homepage shown side-by-side, with the right side showing the site in dark forced color mode.

Those of you that have been doing web development for a bit longer might remember that we could style things with "system colors". That's no longer possible due to security implications, but in the forced colors mode a subset of them are back:

  • CanvasText: the color of text content.
  • LinkText: the color of links.
  • GrayText: the color of disabled content
  • HighlightText: the color of selected text.
  • Highlight: the background color of selected text.
  • ButtonText: the color of a <button> element's text.
  • ButtonFace: the color of a <button> element's background.
  • Canvas: controls the color of the background.

These color names are not here for you to pick and choose, high color mode already overwrite all your text, background, border and button colors. They're there for you to use on other elements (like icons) to make them fit the rest of the site.

Forced-colors has two possible values:

@media (forced-colors: none) {
}

@media (forced-colors: active) {
}

When forced-colors is active, prefers-color-scheme can be used to detect if there is white text on a black background, or black text on a white background.

On Windows, forced-colors will also activate prefers-contrast with a value of more, less or custom depending on whether you have high, low or average contrast between the text and background colors.

Overwriting forced colors

It's not always desirable for forced-colors to overwrite all the colors, for example in a color selector on an ecommerce website. To disable forced colors for specific elements you can use the css property forced-color-adjust: none;.

-ms-high-contrast (supported in old Edge)

-ms-high-contrast was the implementation of forced colors in the old EdgeHTML version of Edge. It has three possible values to check for, which includes the color scheme:

@media (-ms-high-contrast: active) {
}

@media (-ms-high-contrast: black-on-white) {
}

@media (-ms-high-contrast: white-on-black) {
}

It similarly has color keywords for you to use, but they're slightly different:

  • WindowText: the color of text content.
  • -ms-hotlight: the color of links.
  • GrayText: the color of disabled content
  • HighlightText: the color of selected text.
  • Highlight: the background color of selected text.
  • ButtonText: the color of a <button> element's text.
  • ButtonFace: the color of a <button> element's background.
  • Window: controls the color of the background.

Light-level (no support)

Light-level comes with three possible values: dim, normal and washed. What those mean exactly and when they get triggered is up to the operating system.

@media (light-level: dim) {
}

@media (light-level: normal) {
}

@media (light-level: washed) {
}

Light-level will be dim if the screen is in a dark place, like at night, whereas washed means that it's shown under bright lights or in outdoor conditions with lots of sunlight.

Operating systems nowadays already compensate for these situations by increasing or decreasing brightness, but a website owners we can improve the experience more. In dim situations, you might opt to decrease strong contrast here and there and decrease overall brightness. In washed situations you might want to increase the contrast of all text compared to the background.

Scripting (Supported in Firefox 113, Safari 17 and Chromium 120)

Scripting will let you test if JavaScript is available. It has three possible features:

  • none, JavaScript is not available
  • initial-only, JavaScript is only available during page load, but not after
  • enabled, JavaScript is available

Nav controls is used to detect if the page is being viewed in a user agent that has navigation controls (specifically, a back button). It has two possible values:

  • none, meaning there are no navigation controls
  • back, meaning there is are navigation controls and they're visible ("obviously discoverable", according to the spec, which they explain as UI that's visible as opposed to a shortcut or gesture)

This is useful when your website is shown in a webview in an app, where the app might not have its own navigation controls. This way you can still provide a back button or other means of navigating.

Environment-blending (no support)

Environment blending lets you test whether or not your screen blends with the environment, for example if it's projected onto a piece of glass. There's three potential values:

  • opaque. Like a regular monitor, or paper (think of this as the default)
  • additive. For example, a heads-up display like Hololens. Black is transparent, and white is 100% light.
  • subtractive. For example, an LCD display like a Gameboy screen, embedded in a mirror. Here the opposite happens: 'white' is fully transparent and black has the most contrast.

Screen-spanning (supported in Chromium browsers)

The screen-spanning media feature (which used to be called just "spanning") is made to support devices with multiple screens. It will tell you if a browser spans multiple screens and if those screens are aligned horizontally or vertically. It has three possible values:

  • none, when the browser does not span multiple screens.
  • single-fold-horizontal, when the browser spans two screens, with the fold horizontally in the middle.
  • single-fold-vertical, when the browser spans two screens, with the fold vertically in the middle.

The specification for this is not yet final, and it might get new names and values, in particularly being more explicit about spanning horizontally or verticallym with integer values to support more than two screens.

Your site will work fine spanning across two screens because the browser will just pretend that it's a single screen, but you can use the values above, along with a new set of env() css environment variables that show you where the fold is so you can lay out things on both screens (For example you could use it to have a list on one screen and a map on the other.)

  • fold-left
  • fold-top
  • fold-height
  • fold-width

These give you an offset for the fold, and you can combine these with calc() to target each screen individually. For Javascript, there's a new api called window.getWindowSegments() that will return an array of your screens. These are static values so won't update if a user rotates their device.

New notations in Media query levels 4 and 5

Alongside the new media queries, the latest specifications (4 and 5) also comes with some new notations. They're useful improvements that are slowly finding their way into browsers.

Range: Replacing min-* and max-*

In media query level 4 you can do away with the min-* and max-* versions of media features that have a "range context" (e.g., a min and max version) and instead write them as a simple equation.

/* old notation */
@media (min-width: 300px) {
}

/* new notation */
@media (width >= 300px) {
}

If you have both a min-width and a max-width, you can even combine them into one equation:

/* old notation */
@media (min-width: 300px) and (max-width: 750px) {
}

/* new notation */
@media (300px <= width <= 750px) {
}

You can use both < and > as well as <= and >=. The first two will exclude the edges (so (width > 300px) works like (min-width:301px)), while the last two will include them (and work the same as the min-* and max-* versions).

The range syntax has been supported in Firefox for a long time, Chromium supports it since 104 and Safari since 16.4.

The next few notations aren't supported anywhere yet.

or keyword

Instead of a comma, you can now also write or just like how you could already write and. The entire query will then evaluate to true as soon as one of them matches.

/* old notation */
@media (min-width: 300px), (orientation: landscape) {
}

/* new notation */
@media (min-width: 300px) or (orientation: landscape) {
}

not() function

You can prepend not to the entire media query, but what if you only want to check a single value? You can do that with the not() notation:

/* instead of this: */
@media (min-width: 300px) and (hover: none) {
}

/* you can also write this: */
@media (min-width: 300px) and (not(hover)) {
}

Custom media queries

Custom media queries let you define a media query once and then use that media query in multiple places without repeating yourself, just like how css custom properties ("variables") work. They even have the same notation.

Here's an example:

/* Define your custom media query */
@custom-media --small-screen (max-width: 768px);

/* Then use it somewhere in your stylesheet */
@media (--small-screen) {
}

/* You can also combine it with other media features */
@media (--small-screen) and (pointer: fine) {
  /* styling for small screens with a stylus */
}

Deprecated media queries

Despite all the new features coming out, there's also parts older specifications that are not recommended for use anymore. You might still see them in the wild here and there, but you shouldn't add them to new projects and if you find them in existing projects, try and refactor them.

Device-* media queries

When media queries were first implemented, the main idea was that they reasoned about the device, so there were media features like device-width, device-height and device-aspect-ratio.

But the size of the device is not always the size of the viewport, so styling based on these makes no sense.

prefers-color-scheme lost a value

The no-preference value for prefers-color-scheme was recently removed from the specification so there will only be dark or light as possible values. They removed it because most browsers implemented this media feature with the default being light and never implemented no-preference anyway. The door is left open for re-introducing it in the future, as well as introducing other color scheme preferences, such as "sepia".

Scan

No browser actually supports this feature and it was to be used in tandem with the deprecated tv media type so we'll probably never see support again, but I'll explain it anyway. The scan media query can be used to test for the scanning process that is used to paint an image on a screen (like on a CRT monitor). It has two options:

  • interlace, where odd and even lines are drawn alternately.
  • progressive where all lines are drawn one by one.

progressive is described as a screen that is slower to update and probably fuzzier.

Using media queries in JavaScript

Lastly, I want to point out that you can't just use media queries in CSS, you can reason about them in JavaScript as well. You can do this with the window.matchMedia() function.

The window.matchMedia() function takes a media query string and return a "MediaQueryList" object with information on if that particular media query matches:

const match = window.matchMedia("(min-width: 400px)");

// match output:
{
  matches: true,
  media: "(min-width: 400px)",
}

The matches value will tell you if the media query evaluates to true. So if you had video that you wanted to autoplay, you could use a matchMedia function to do that only for people that don't have prefers-reduced-motion: reduce:

const video = document.createElement('video');
const canAutoPlay = window.matchMedia('(prefers-reduced-motion: no-preference)').matches;

video.setAttribute('autoplay', canAutoPlay);

You can also add a listener to the MediaQueryList object. This will let you respond to changes in the document that cause the media query to go from false to true, or the other way around.

const match = window.matchMedia('(min-width: 400px)');

match.addEventListener('change', (e) => {
  if (e.matches) {
    /* do a thing */
  } else {
    /* do another thing */
  }
});

The matchMedia object also used to have an addListener function that you could use for the same purpose, but that has been deprecated.

Test your media queries with Polypane, the browser for developers and designers

Polypane lets you test your website in many different screen sizes and against different media queries like prefers-color-scheme, print stylesheets and prefers-reduced-motion.

It's the fastest way to develop and test websites, and you can try it for free!

Build your next project with Polypane

  • Use all features on all plans
  • On Mac, Window and Linux
  • 14-day free trial – no credit card needed
Try for free
Polypane screenshot