Cross-posted from my personal blog. Status: third draft, corrections still appreciated!
As browsers are starting to ship parts of the Media Queries Level 5 spec, recent discussion on CSS media queries understandably revolves around using these new features to better adjust web pages to users' needs and preferences. In 2019, the old-school queries published in 2012 as a W3C Recommendation, and near-universally supported across browsers, seem to have been exhaustively dissected and discussed.
Still, I felt some aspects of how they work continued to elude me. In this article I set out to clarify them.
Quick recap: media queries are a mechanism in CSS and HTML (with additional hooks for JavaScript) which lets us test certain aspects of the browser and device that display our web page. These aspects are external to the page, and not (usually) influenced by the styles we apply to it.
There are many features we can test. I'm going to look at a tiny slice: the width
and height
media features, with their associated min-
and max-
queries. They tell us things about the size of the space allocated for the web page.
A media query inside CSS uses the @media
rule:
/*
Makes the page red whenever there are
at least 400px available for it in the browser.
*/
@media (min-width: 400px) {
html {
background: red;
}
}
When we qualify the width
feature with the min-
prefix, the query reads as at least, while the max-
stands for at most.
The Media Queries Level 4 specification introduces a clearer syntax:
(width >= 400px)
instead of(min-width: 400px)
. Since it's a relatively new addition, it's not a good replacement for the classic syntax yet.
Are dimension queries useful?
Knowing what constraints the browser imposes on our page is useful for making adjustments to the layout to better adapt it to the myriad of screens on which it can potentially be displayed. Media queries have been pivotal to propelling Responsive Web Design into ubiquity.
With new, more powerful, ways of expressing layout such as Flexible Box and Grid, CSS gains alternatives to media queries for responsive layouts. Every Layout by Heydon Pickering and Andy Bell is an excellent resource to get a feel for using flex
and grid
properties, often combined with calc()
, for common patterns normally solved by media queries. CSS is also getting the min()
, max()
, and clamp()
comparison functions, which extend min-width
/max-width
/min-height
/max-height
to all properties, and promise to further erode dimension queries' territory.
Although these recent developments don't make dimension queries obsolete, they relegate them to an auxiliary role in responsive design. That's a good thing! Media queries are coarse, and best suited to make top-level adjustments based on top-level constraints.
Dimension queries are not strictly a CSS thing, eiher. In HTML, width queries also show up in the sizes
attribute on <img>
and <source>
elements to enable responsive images.
All in all, it seems we can't Marie Kondo them out of our web design toolbox just yet, so let's see how we can use dimension queries efficiently. From here on, I'm going to call them just media queries, since they're the only ones discussed.
Units in media queries
How does our choice of CSS units in media queries influence our design, and our users' ability to express preferences for their experience?
Devices have screens made out of pixels. The browser takes part of those device pixels to display a web page in. The narrower the browser, the fewer pixels we get. CSS has the px
unit, short for pixel. For the sake of simplicity let's gloss over, for a short while, how CSS pixels are not the same thing as device pixels. They sure feel, at first brush, like they're the same.
Writing media queries in pixels is pretty straightforward, feels intuitive (especially coming from graphic design tools), and produces the result we expect. At least on the screen for which we've designed it.
But px
in media queries, and in general, go against the grain of the web. Content should flow like water regardless of the vessel holding it, and pixels make it akin to a lifeless lump of coal.
CSS gives us font-relative CSS units — em
, rem
, and their friends — which allow us to write styles in harmony to the content we want to display. When used in media queries, like we intuit we should, what do they relate to exactly?
According to the spec, they relate to the initial value of font properties. For em
and rem
, the relevant property is font-size
, which most browsers initially set to 16px
.
As such, changing the font size of the html
element:
html {
font-size: 1.25rem;
}
...detaches the meaning of rem
s in your styles from the meaning of rem
s in media queries: 1rem
in styles is now equivalent to 20px
, while in media queries 1rem
, and 1em
for that matter, is still 16px
.
Why would the spec mandate this in the first place?
The CSS Working Group explain in a FAQ entry that selectors can't depend on layout. If rem
/ em
media queries depended on the font-size
of the html
element, you could create an infinite loop:
html {
font-size: 1rem;
}
@media (min-width: 60rem) {
html {
/*
Setting this invalidates
the media query selector
that triggered this style.
*/
font-size: 10rem;
}
}
This makes things a bit more cumbersome, but the limitation makes sense. You take a mental note of this peculiarity, and make sure you always think of media queries in terms of initial sizes. Suddenly,
The trouble with Safari
Current browsers generally adhere to the spec in regards to font-relative units in media queries. The big outlier is Safari, on both desktop and mobile.
Safari follows the spec for most relative units: 1em
in media queries is 16px
regardless of the font size on the html
element. But it scales rem
s in particular in accordance to the html
element (WebKit Bug 156684). Since this breaks the "no layout-dependent selectors" CSS rule, we can actually witness the infinite loop described above.
Got us there, Safari! But since we can use em
and rem
interchangeably, as they relate to the same thing in any spec-respecting browser, we just pick the not-broken one.
If we stick to
em
in media queries, we bring Safari's behavior in line with the other browsers.
User preferences
Users have a few ways of adjusting their experience of a web page. Let's go through them, one by one, to see their impact on our choice of units for media queries.
Zooming in
Remember the whole pixels are not pixels thing we avoided earlier? It becomes key to how zoom works. On screens, relative units resolve to px
. These are CSS pixels, distinct from physical pixels on the device. They map to physical pixels based on the device's pixel density. You can read more about it in CSS Length Explained, but what matters is there's a certain ratio between what constitutes a pixel in CSS and on the device, so that you can experience one CSS pixel roughly the same across devices.
When you change the zoom level, modern browsers will tweak the ratio between CSS pixels and device pixels. 1px in CSS ends up meaning two device pixels, or four, or half a pixel. This is a brilliant way to keep the layout mostly intact, regardless of the choice of CSS units in stylesheets.
1rem
is still 16px
when you zoom in, but now there are fewer (CSS) pixels available to your page. This reflects in the media queries: min-width
has a lower threshold — fewer pixels, fewer rem
s, fewer em
s. Suddenly,
The trouble with Safari (again)
Safari on macOS has a bug where em
and rem
units in media queries get the browser's zoom level factored in (WebKit Bug 156687).
As you zoom in, 1rem
becomes 20px
, and then 28px
in media queries. This is concerning because the zoom level is now doubly-represented: once by the fact that we get fewer CSS pixels for the page, and again by it inflating our em
and rem
s.
With iOS 13, and the new iPadOS, Safari also introduced zoom controls for mobile users. They work much better to adjust the layout than the Request Desktop/Mobile Website feature, which does nothing on websites built on responsive design principles. They're part of the reason I wanted to learn more about media queries and zooming.
Thankfully, the zoom controls on iOS 13 work in accordance to the rest of the browsers. (But a glance at desktop Safari 13, currently in the Technology Preview stage, reveals it still exhibits the bug.)
Since it's isolated to desktops (thus, larger screens), the behavior macOS Safari is not the end of the world. As the user zooms in, Safari thinks it has fewer em
s available than it actually has, and triggers a mobile-friendly layout sooner than it needs to — no biggie. When zooming out, what can happen is the layout shrinks disproportionately to the text, and may warrant some extra attention.
Fun with vw
. Safari on macOS applies the zoom level to all relative units, and that includes vw
. Yes, min-width
and max-width
don't always match 100vw
. Why, that means we can use media queries to detect the zoom level!
We can use this quirk to our advantage, and employ media queries to fix any broken aspects of the layout in zoomed-out Safari. Since vw
media queries are not affected by the element's font size the way rem
s are, we can, if that helps in any way, go ahead and adjust it:
@media (min-width: 133.33vw) {
/* the zoom level in Safari is at most 75% */
html {
/* Something smaller than usual */
font-size: 0.9em;
}
}
In the example above, 133.33
comes from dividing 100 with the maximum zoom level we want to match, in our case 0.75
(75%).
Adjusting the zoom level is common, as it's readily available in menus and via keyboard shortcuts, and browsers do a good job of honoring it.
Changing the font settings
Users have other points of leverage hidden among the browser preferences — adjusting how text is displayed on web pages. It comes in (at least) two distinct flavors:
Changing the default size
So far we haven't really examined an assumption we made earlier: that the initial font size in browsers is 16px
.
According to research by Evan Minto, around 3% of users navigate the web at other sizes, either because browsers themselves have a default size other than 16px
, or the user has changed the default.
The distinction doesn't matter, as both have the same effect. Whatever the initial font size the browser offers (either its default, or a user preference) becomes the basis of media queries, and the initial value for the html
element's font size. It's just not always 16px
.
At this point, it's worth noting the consequences of absolute units for the html
font size. Using html { font-size: 12px; }
forces a size on the page that outright ignores the user preference.
In addition to being insensitive to the user, we further (and unpredictably) detach the notion of 1em
in media queries — which, remember, are still based on that preference — from what's actually displayed on the page.
Rather, think about it in terms of:
Do I want my page, as a whole, to be typeset larger/smaller than, or largely the same as, the average experience the user has?
...and then use font-relative units for the html
font size to express it. These units tweak the user preference rather than dismiss it altogether.
Setting a minimum font size
As a supplement to the default font size, browsers can also impose a minimum font size.
This does not normally¹ affect the initial font size. The font-relative media queries and font-size
declarations still use the initial font size as the basis. But at render time, text on the page will have the minimum baked in, and possibly display larger than we typeset it.
When the minimum font size kicks in, browsers behave slightly differently. In Firefox, style declarations other than font-size
using em
and rem
remain unaffected — they use the original computed size, before the adjustment. Chrome and Safari, on the other hand, will trickle the font size adjustment to other properties as well, so for example a padding of 1em
around a text will remain proportional if the size is increased as a result of the minimum font size.
¹ Safari comes with a single setting called never use font sizes smaller than X. It gets factored into the initial font size, affecting media queries in ways I can't quite make heads and tails of, so... it's left as an exercise to the reader? :-)
Text-only zoom
Firefox has a Zoom text only feature which alters the way zoom works. It disables the scaling of CSS pixels, and instead factors the zoom level into the initial font size.
That means that media queries using font-relative units (em
, rem
) get a new basis, matching the initial font size of the <html>
element.
To really drive the feature home, and make it work as expected on pages which might use an absolute font size on the <html>
element, it also factors the zoom level into the font-size
computed value.
Everything works splendidly. The big losers here are px
queries. When you zoom in, the content gets bigger and bigger, and nothing changes in queryland, since there's no scaling of CSS pixels.
One less reason to ever use them!
Conclusion
Some takeaways from this foray into media queries:
px
-based media queries have little connection to the content displayed on the page, and are best avoided. They also fail to respond to the user's preferences about font size, and can't handle Firefox's text-only zoom.
When it comes to font-relative CSS units, your best bet for predictable cross-browser behavior is to use em
in media queries. It avoids the problem with rem
in desktop Safari, but otherwise they're interchangeable. Zoom out of the page in macOS Safari to check that it does not break.
When adjusting the font size on the html
element, use font-relative units to respect the user's preferences. And keep in mind that by changing it from the default, you're slightly shifting the meaning of 1rem
in styles vs. 1rem
/ 1em
in media queries.
It's hard to obtain an intuition on the way zoom levels and user preferences interact with one another, and the truth is always in the pudding. So be sure to test your page in a variety of scenarios involving different browsers, zoom levels, and font settings, where available.
Thank you to Simon Pieters for corrections & guidance in navigating the W3C specs.
Appendix: Deprecated media queries
CSS Media Queries 4 deprecates the use of device-width
, device-height
, and device-aspect-ratio
, which previously referred to physical pixels. Instead, browsers should start reporting them in CSS pixels, which Firefox has already started doing (Edge seems to do so as well, but I can't tell exactly in Browserstack). Safari and Chrome continue to report physical pixels at the time of writing.
To CSS authors, these queries are not recommended.
Appendix: Methodology
I made a diagnostics page to observe what information browsers expose to CSS and JavaScript APIs:
👉 Browser Summary 👈
So far I have looked at the browsers I had at hand:
- Firefox macOS
- Chrome macOS
- Safari macOS 12 & 13 (Technical Preview)
- Safari iOS 13
- Safari iPadOS
In addition, I've used Browserstack to check:
- Internet Explorer 11
- Microsoft Edge (I couldn't find a way to change the default font settings from within Edge itself, so I'm not 100% sure about their impact)
Measuring Media Queries
Where do the values for min-width
, min-height
, et cetera come from on the diagnostics page?
JavaScript has access to media queries via the CSS Object Model API. One particular feature is we can match media queries from JavaScript using the Window.matchMedia()
method:
let query = window.matchMedia('(min-width: 10rem)');
if (query.matches) {
// ...
} else {
// ...
}
This allows us to check min-width
against a certain value that matters to us, and see if it matches or not. But, for the diagnostics page, I needed to find the actual breakpoint beyond which a query (e.g. min-width
) stops matching — that is, the reverse of what matchMedia()
was designed for.
Technically, the CSSOM View Module spec adds JS-accessible proxies for various measurements, but just to rule out possible inconsistencies in how browsers report these values vs. the actual pivotal points in media queries, I opted to obtain the numbers straight from the proverbial horse's mouth.
To do that, we can (ab)use matchMedia
to learn the breakpoint of our current browser/device by asking repeatedly with different values. I used the bisection method to avoid making a gazillion queries:
function find_min_width() {
let start = 0; // 0 px
let end = 1000000; // 1 million px
let precision = 1; // whole pixels
while (end - start >= precision) {
let midpoint = start + (end - start) / 2;
let query = matchMedia(`(min-width: ${midpoint}px)`);
if (query.matches) {
start = midpoint;
} else {
end = midpoint;
}
}
return Math.round(start);
}
This function returns the breakpoint value, in pixels, of the min-width
media query for our current environment. The same technique, with some adjustments to the precision, can be used for em
, rem
, and vw
.
Top comments (0)