The @media rule is actually a long-standing feature of CSS. Its syntax was originally defined by the CSS 2 specification back in 1998. Building on the media types defined by HTML 4, @media enabled developers to serve different styles to different media types—such as print or screen .
The Media Queries Level 3 specification extended the @media rule to add support for media features in addition to media types. Media features include window or viewport width, screen orientation, and resolution. Media Queries Level 4 added interaction media features, a way to apply different styles for pointer device quality—that is, the fine-grained control of a mouse or stylus versus the coarseness of a finger. Media Queries Level 5 adds features for Light- Level and scripting , along with user preference media features such as prefers-reduced- motion and prefers-reduced-transparency .
Alas, most of the media types defined by HTML 4 are now obsolete. Only aLL , screen , print , and speech are currently defined by any specification. Of those, only aLL , screen and print have widespread browser support. We’ll briefly discuss them in the examples that follow. As for media features, we’ll focus on what’s available in browsers today.
1. Media Query Syntax: The Basics
Media query syntax seems simple, but sometimes it’s a bit counterintuitive. In its simplest form, a media query consists of a media type, used alone or in combination with a media condition-such as width or orientation . A simple, type-based media query for screens looks like this:
@media screen {
/* Styles go here */
}
CSS style rules are nested within this @media rule set. They’ll only apply when the document is displayed on a screen, as opposed to being printed:
@media screen {
body {
font-size: 20px;
}
}
In the example above, the text size for this document will be 20px when it’s viewed on a desktop, laptop, tablet, mobile phone, or television.
We can apply CSS to one or more media types by separating each query with a comma. If the browser or device meets any condition in the list, the styles will be applied. For example, we could limit styles to screen and print media using the following:
@media screen, print {
body {
font-size: 16px;
}
}
The real power of media queries, however, comes when you add a media feature. Media features interrogate the capabilities of the device or conditions of the viewport.
A media feature consists of a property and a value, separated by a colon. The query must also be wrapped in parentheses. Here’s an example:
/* Note that this is the equivalent of @media all (width: 480px) */ @media ( width: 480px ) {
nav li {
display: inline-block;
}
}
Now nav li will have a display value of inline-block only when the width of the viewport is equal to 480 pixels. Let’s use the and keyword to make a more specific media query:
@media screen and ( width: 480px ) {
nav li {
display: inline-block;
}
}
These styles will be used only when the output device is a screen and its width is 480px . Notice here that the media type is not enclosed by parentheses, but the media feature— (width: 480px) —is.
But the query above has a small problem. If the viewport is wider than 480px or narrower than 480px —and not exactly 480px —these styles won’t be applied. What we need instead is a range.
2. Range Media Features and min- and max- Prefixes
A more flexible media query might test for a minimum or maximum viewport width. We can apply styles when the viewport is at least this wide, and no more than that wide. Luckily for us, the Media Queries Level 3 specification defines the min– and max– prefixes for this purpose. These prefixes establish the lower or upper boundaries of a feature range.
Let’s update our previous code:
@media ( max-width: 480px ) {
nav li {
display: block;
}
}
In this example, nav li will have a display property value of block from a viewport width of 0 , up to and including a maximum viewport width of 480px .
We can also define a media query range using min- and max- , along with the and keyword. For example, if we wanted to switch from display: block to display: flex between 481px and 1600px , we might do the following:
@media ( min-width: 481px ) and ( max-width: 1600px ) {
nav ul {
display: flex;
}
}
If both conditions are true—that is, the viewport width is at least 480px , but not greater than 1600px —our styles will apply.
Not all media feature properties support ranges with min- and max– . The table below lists those that do, along with the type of value permitted for each.
Firefox versions 63 and above support comparison operators such as > and <= in addition to the min and max syntax for ranges. Instead of @media (min-width: 480px) and (max- width: 1600px) , we could write this query as follows:
@media ( width >= 480px ) and ( width <= 1600px ) {
nav li {
display: block;
}
}
That’s a little clearer than @media ( min-width: 480px ) and ( max-width: 1600px ) . Unfortunately, this syntax isn’t yet supported by most browsers. Stick with min- and max- for now.
3. Discrete Media Features
There’s a second type of media feature: the discrete type. Discrete media features are properties that accept one of a set—or a predefined list—of values. In some cases, the set of values is a Boolean—either true or false. Here’s an example using the orientation property. The example adjusts the proportional height of a logo when in portrait mode:
@media screen and ( orientation: portrait ) {
#logo {
height: 10vh;
width: auto;
}
}
The orientation feature is an example of a discrete media feature. It has two supported values, portrait and Landscape . Minimum and maximum values don’t make much sense for these properties. The table below lists discrete media features that are currently available in major browsers.
Other discrete media features include overflow-block and overflow-inline , which describe the behavior of the device when content overflows in the block or inline direction (think electronic billboards or slide shows). Eventually, we may also see support for a scripting feature which tests for JavaScript support.
One discrete media feature we can use now is hover (along with any-hover ). The hover media feature query allows us to set different styles based on whether or not the primary input mechanism supports a :hover state. The any-hover feature works similarly, but applies to any input mechanism, not just the primary one. It’s a discrete feature type, and has just two valid values:
- none : the device has no hover state, or has one that’s inconvenient (for example, it’s available after a long press)
- hover : the device has a hover state
Consider the case of radio buttons and checkbox form controls on touchscreens. Touchscreen devices typically have an on-demand hover state, but may lack one completely. Adult-sized fingers are also fatter than the pointers of most mouse or track pad inputs. For those devices, we might want to add more padding around the label, making it easier to tap:
@media screen and ( hover: on-demand ) {
input[type=checkbox] + label {
padding: .5em;
}
}
Another media feature that’s well supported by browsers is the pointer media feature (and any-pointer ). With pointer , we can query the presence and accuracy of a pointing device for the primary input mechanism. The any-pointer property, of course, tests the presence and accuracy of any pointer available as an input mechanism. Both media features accept one of the following values:
- none : the device’s primary input mechanism is not a pointing device
- coarse : the primary input mechanism is a pointing device with limited accuracy
- fine : the device’s primary input mechanism includes an accurate pointing device
Devices with pointing inputs include stylus-based screens or pads, touchscreens, mice, and track pads. Of those, touchscreens are generally less accurate. Stylus inputs, on the other hand, are very accurate—but, like touchscreens, they lack a hover state. With that in mind, we might update our hover query from earlier so that we only add padding when the pointer is coarse :
@media screen and ( hover: none ) and ( pointer: coarse ) {
input[type=checkbox] + label {
padding: .5em;
}
}
Most operating systems include a set of accessibility and user preference settings that control features like the animation and transparency of windows, or system-wide theming preferences. Level 5 of the Media Queries specification defines several features for querying user-preference settings: prefers-reduced-motion , prefers-coLor-scheme , prefers- contrast , prefers-reduced-transparency , prefers-reduced-data and forced-colors. Of these, only prefers-reduced-motion and prefers-coLor-scheme have widespread support across browsers and operating systems.
4. Using prefers-reduced-motion to Improve the Experience of People with Vestibular and Seizure Disorders
As mentioned in Chapter 7, “Transitions and Animations”, large-scale animations can create sensations of dizziness and nausea for people with vestibular disorders. Flickering animations can cause seizures for people with photosensitive epilepsy.
Seizures and dizziness don’t make for a very good user experience. At the same time, animation can improve usability for users who aren’t affected by vestibular disorders. As a way to balance improved usability for some while preventing debilitating conditions in others, WebKit proposed a prefers-reduced-motion media feature. It has two possible values: nopreference and reduce .
With prefers-reduced-motion , we can provide an alternative animation or disable it altogether, as shown in the following example:
/* Starting state */
.wiggle {
animation: wiggling 3s ease-in infinite forwards alternate;
}
@media screen and ( prefers-reduced-motion: reduce ) {
.wiggle {
animation-play-state: paused;
}
}
If the user’s preference is to reduce motion, the .wiggle animation will be disabled.
When used without a value, prefers-reduced-motion is true. In other words, removing reduce from the above media query gives it an equivalent meaning:
@media screen and ( prefers-reduced-motion ) {
.wiggle {
animation-play-state: paused;
}
}
Even when the user has chosen to reduce motion, your animations won’t be disabled unless you add CSS to accommodate that preference. You may instead wish to enable transitions and animations only when the user hasn’t indicated a preference:
/* @media screen and not ( prefers-reduced-motion ) also works */
@media screen and ( prefers-reduced-motion: no-preference ) {
.wiggle {
animation: wiggling 3s ease-in infinite forwards alternate running;
}
}
Chrome versions 73 and below, Firefox versions 62 and below, Edge versions 18 and below, and Safari versions 10.1 and below don’t support prefers-reduced-motion . Consider adding a user interface element that lets site visitors disable animations in those browsers. Don’t forget to follow WCAG guidelines when creating animations and transitions.
5. Respecting Users Color Preferences with prefers-color-scheme
Some operating systems offer the ability to select a dark theme for the interface. We can use the prefers-coLor-scheme feature to add support for this preference in web pages and applications. This feature has two possible values: Light and dark .
@media ( prefers-color-scheme: dark ) {
/* Styles here */
}
The prefers-color-scheme feature works well with custom properties (see Chapter 4). For example, you might use a different color palette for each color scheme, and use custom properties to define each color:
/* Styles when there’s no preference */
:root {
–background: #ccc;
–foreground: #333;
–button-bg: #505;
–button-fg: #eee;
–link: #909;
–visited: #606;
}
/* Update colors for the background, foreground, and buttons */ @media screen and ( prefers-color-scheme: light ) {
:root {
–background: #fff;
–foreground: #000;
–button-bg: #c0c;
–button-fg: #fff;
}
}
@media screen and ( prefers-color-scheme: dark ) {
:root {
–background: #222;
–foreground: #eee;
–button-bg: #808;
–button-fg: #fff;
–link: #f0f;
–visited: #e0f;
}
}
When creating themes for use with prefers-coLor-scheme , don’t forget to check whether your foreground and background colors have sufficient contrast. Firefox, Chrome and Edge have robust accessibility checking tools built into their developer tools. Deque provides axe , a free developer tools browser extension for Firefox, Chrome and Edge that includes checks for color contrast. Tools such as Lea Verou’s Contrast Ratio also work well.
To develop and test for dark mode, you’ll first need to enable it:
- Windows: go to Settings > Personalization > Colors > Choose your color
- macOS: go to System Preferences > General > Appearance
- Ubuntu: go to Settings > Appearance > Window colors
Chrome and Edge also allow users to set a preference at the browser level by enabling the Force Dark Mode for Web Contents setting. You can find this setting at chrome://fLags/#enabLe-force-dark and edge://fLags/#enabLe-force-dark , respectively. Keep in mind that even when Force Dark Mode is enabled, matchMedia(‘(prefers-dark- mode)’).matches may return false for some operating systems.
In Firefox, you can simulate light (via the sun-shaped icon) and dark (via the crescent- moon-shaped icon) color scheme support in the web inspector panel.
A third value, no-preference , has been removed from the specification due to a lack of browser support. You may, however, come across articles or code samples that include it. Don’t use it in new projects.
6. Nesting @media Rules
It’s also possible to nest @media rules. Here’s one example where it might be useful to nest media queries:
@media screen {
@media ( min-width: 320px ) {
img{
display: block;
width: 100%;
height: auto;
}
}
@media ( min-width: 640px ) {
img {
display: inline-block;
max-width: 300px;
}
}
}
In this example, we’ve grouped all our screen styles together, with subgroups for particular viewport widths.
7. Working around Legacy Browser Support with only
As mentioned in the beginning of this chapter, @media has been around for a while. However, the syntax and grammar of @media has changed significantly from its original implementation. As the Media Queries Level 4 specification explains , the original errorhandling behavior:
would consume the characters of a media query up to the first nonalphanumeric character, and interpret that as a media type, ignoring the rest. For example, the media query screen and (coLor) would be truncated to just screen .
To avoid this, we can use the only keyword to hide media queries from browsers that support the older syntax. The only keyword must precede a media query, and affects the entire query:
@media only screen and ( min-resolution: 2dppx ) {
/* Styles go here */
}
8. Negating Media Queries
You can also negate a media query using the not keyword. The not keyword must come at the beginning of the query, before any media types or features. For example, to hide styles from print media, you might use the following:
@media not print {
body {
background: url( ‘paisley.png’ );
}
}
If we wanted to specify low-resolution icons for lower-resolution devices instead, we might use this snippet:
@media not print and ( min-resolution: 1.5dppx ) {
.external {
background: url( ‘arrow-lowres.png’ );
}
}
Notice here that not comes before and negates the entire media query. You can’t insert not after an and clause. Arguments such as @media not print and not (min-resolution: 2dppx) or @media screen and not (min-resolution: 2dppx) violate the rules of media query grammar. However, you can use not at the beginning of each query in a media query list:
@media not ( hover: hover ), not ( pointer: coarse ) {
/* Styles go here */
}
Styles within this grouping rule would be applied when the device doesn’t have a hover state or when the pointing device has fine-grained accuracy.
9. Other Ways to Use Media Queries
Thus far, we’ve talked about @media blocks within stylesheets, but this isn’t the only way to use media types and queries. We can also use them with either @import or the media attribute. For example, to import a stylesheet typography.css when the document is viewed on screen or printed, we could use the following CSS:
@import url( typography.css ) screen, print;
But we can also add a media query to an @import rule. In the following example, we’re serving the hi-res-icons.css stylesheet only when the device has a minimum pixel density of 2dppx :
@import url( hi-res-icons.css ) ( min-resolution: 2dppx );
Another way to use queries is with the media attribute, which can be used with the <style> , <Link> , <video> , and <source> elements. In the following example, we’ll only apply these linked styles if the device width is 480 pixels wide or less:
<link rel=”stylesheet” href=”styles.css” type=”text/css” media=”screen and (max-width: 480px)”>
We can also use the media attribute with the <source> element to serve different files for different window widths and device resolutions. What follows is an example using the <source> element and media attribute with the <picture> element:
<picture>
<source srcset=”image-wide.jpg” media=”( min-width: 1024px )”>
<source srcset=”image-med.jpg” media=”( min-width: 680px )”>
<img src=”image-narrow.jpg” alt=”Adequate description of the image contents.”>
</picture>
10. Content-driven Media Queries
A current common practice when using media queries is to set min-width and max-width breakpoints based on popular device sizes. A breakpoint is the width or height that triggers a media query and its resulting layout changes. Raise your hand if you’ve ever written CSS that resembles this:
@media screen and ( max-width: 320px ) {
⋮
}
@media screen ( min-width: 320px ) and ( max-width: 480px ) {
⋮
}
@media screen ( min-width: 481px ) and ( max-width: 768px ) {
⋮
}
@media screen ( min-width: 769px ) {
⋮
}
These work for a large number of users. But device screen widths are more varied than this. Rather than focus on the most popular devices and screen sizes, try a content-centric approach.
A content-centric approach to media queries sets breakpoints based on the point at which the layout starts to show its weaknesses. One strategy is to start small, which is also known as a mobile-first approach. As Bryan Reiger puts it, “the absence of support for @media queries is in fact the first media query”14.
You can do a lot to create a flexible, responsive layout before adding media queries. Then, as you increase the viewport width or height, add styles that take advantage of the additional real estate. For example, how wide is the browser window when lines of text become too long to read comfortably? That can be the point at which your layout switches from a singlecolumn layout (as illustrated in the first image below) to a two-column layout (shown in the second image).
There are two advantages to this approach. First, your site will still work on older mobile browsers that lack support for media queries. The second reason is just as important: this approach prepares your site for a wider range of screen widths and resolutions.
11. Using Media Queries with JavaScript
Media queries also have a JavaScript API, better known as matchMedia() . If you’re not versed in JavaScript, don’t worry. We’ll keep the examples short so they’re easier to understand. The API for media queries is actually defined by a different specification, the CSSOM View Module . It’s not CSS, strictly speaking, but since it’s closely related to @media , we’ll cover it.
The matchMedia() method is a property of the window object. That means we can refer to it using window.matchMedia() or just matchMedia() . The former is clearer, since it indicates that this is a native JavaScript method, but the latter saves a few keystrokes. I’m a lazy typist, so I’ll use matchMedia() in the examples that follow.
Use matchMedia() to test whether a particular media condition is met. The function accepts a single argument, which must be a valid media query.
Why use a media query with JavaScript rather than CSS? Perhaps you’d like to display a set of images in a grid on larger screens, but trigger a slide show on small screens. Maybe you want to swap the src value of a <video> element based on the screen size or resolution. These are cases for using matchMedia() .
Here’s a simple example of matchMedia in action. This code checks whether the viewport width is greater than or equal to 45em :
var isWideScreen = matchMedia( “(min-width: 45em)” );
console.log( isWideScreen.matches ); // Logs true or false to console
Using matchMedia() creates a MediaQueryList object. Here, that object is stored in the isWideScreen variable. Every MediaQueryList object contains two properties:
- media , which returns the media query argument that was passed to matchMedia()
- matches , which returns true if the condition is met and false otherwise
Since we want to know whether it’s true that the browser window is at least 45em wide, we need to examine the matches property.
MediaQueryList.matches will return false when either:
- the condition isn’t met at the time matchMedia() is invoked
- the syntax of the media query is invalid
- the browser doesn’t support the feature query
Otherwise, its value will be true .
Here’s another example of using matchMedia . We’ll update the source of a <video> element based on the size of the current viewport and resolution:
if( matchMedia( “( max-width: 480px ) and ( max-resolution: ldppx )” ) {
document.querySelector(‘video’).src = ‘smallvideo.mp4’;
}
If the condition doesn’t match—or the browser doesn’t support the resolution feature query—the value of src won’t change.
12. Error Checking with not all
Typically, the value of the media property is the media query we’ve tested. But maybe you forgot to include the parentheses around your feature query (a syntax error). Or perhaps the query uses a pointer feature query, but the browser is yet to support it. In both of those cases, the browser will return a not aLL value. This is media query speak for “this doesn’t apply to any media condition”.
In cases where the media query is a list—that is, when it contains multiple conditions—the value of matchMedia().media will also contain multiple values. If part of that query list is invalid or unsupported, its value will be not all . Here’s an example:
var mq = matchMedia( “( hover: none ), ( max-width: 25em )” );
In browsers lacking support for the hover: none media feature query, the value of mq.media will be not all, (max-width: 25em) . In browsers that do support it, the value of mq.media will be (hover: none), (max-width: 25em) . Let’s look at another example:
var mq = matchMedia( “min-resolution: 1.25dppx, ( max-width: 25em )” );
In this example, the value of mq.media will also be not all, ( max-width: 25em ) . In this case, however, it’s because our first feature query uses the wrong syntax. Remember that media feature queries need to be enclosed in parentheses. The argument should be matchMedia( “( min-resolution: 1.25dppx ), ( max-width: 25em )” ); instead.
13. Listening for Media Changes
Media conditions aren’t necessarily static. Conditions can change when the user resizes the browser or toggles between portrait and landscape mode. Luckily, there’s a mechanism for monitoring and responding to changes in our document’s environment: the addEventListener() method.
The addEventListener() method is a standard method of the Document Object Model. It accepts two arguments: the event type, and a callback function. The callback function gets invoked every time an event of the specified type occurs. Changes to the document’s environment are always change events.
Let’s add a class name when our document enters landscape orientation. The first step is to create a MediaQueryList object using matchMedia and a media query:
var isLandscape = matchMedia( “( orientation: landscape )” );
Step two is to define our callback function. The callback function receives an object as its only argument. In Chrome, Safari and Microsoft Edge, this will be a MediaQueryListEvent object. In Firefox (verified in versions 90 and below), it’s a MediaQueryList object, which is a holdover from an earlier version of the specification. There isn’t much difference between them, and the code below works with both object types:
const toggleClass = function ( mediaquery ) {
if ( mediaquery.matches ) {
document.body.classList.add( ‘widescreen’ );
}
else {
document.body.classList.remove( ‘widescreen’ );
}
}
Media query events aren’t very smart. They’re fired any time the value of MediaQueryList.matches changes, regardless of whether or not the condition is true . This means we need to examine the value of the MediaQueryListEvent.matches or MediaQueryListEvent.media property. In this case, if the value of mediaquery.matches is true , we’ll add a class name to our <body> element. Otherwise, we’ll remove it.
Finally, let’s add this event listener to our MediaQueryList object with addEventListener :
isLandscape.addEventListener( ‘change’, toggleClass );
To remove a listener, use removeEventListener as shown:
isLandscape.removeEventListener( toggleClass );
Early versions of the CSSOM View specification defined addListener and removeListener methods. These methods were separate mechanisms, removed from the DOM event queue. This changed in the Level 4 specification. Both functions are now deprecated, but older browsers still support them.
One workaround for this is to test whether the browser supports addEventListener when used with a MediaQueryList object:
if( typeof isLandscape.addEventListener === ‘function’ ) {
isLandscape.addEventListener( ‘change’ , toggleClass );
} else {
isLandscape.addListener( toggleClass );
}
You can use a similar check for removeEventListener and removeListener .
Source: Brown Tiffany B (2021), CSS , SitePoint; 3rd edition.