Making sense of units in CSS Media Queries (in the year 2019)
Tags: webdev, css
Published: 2019-09-26 16:34:37

_Cross-posted from [my personal blog](http://danburzo.ro/media-query-units/). Status: third draft, corrections still appreciated!_

As browsers are starting to ship parts of the [Media Queries Level 5](https://drafts.csswg.org/mediaqueries-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](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries) 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](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries#Media_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](https://developer.mozilla.org/en-US/docs/Web/CSS/@media): 

```css
/*
	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](https://www.w3.org/TR/mediaqueries-4/#mq-range-context): `(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](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Responsive_Design#Media_Queries) into ubiquity.

With new, more powerful, ways of expressing layout such as [Flexible Box](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout) and [Grid](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout), CSS gains alternatives to media queries for responsive layouts. [Every Layout](https://every-layout.dev/) by Heydon Pickering and Andy Bell is an excellent resource to get a feel for using `flex` and `grid` properties, often combined with [`calc()`](https://developer.mozilla.org/en-US/docs/Web/CSS/calc), for common patterns normally solved by media queries. CSS is also getting the `min()`, `max()`, and `clamp()` [comparison functions](https://drafts.csswg.org/css-values-4/#comp-func), 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](https://www.smashingmagazine.com/2018/02/media-queries-responsive-design-2018/). 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](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/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:

```css
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](https://wiki.csswg.org/FAQ#selectors-that-depend-on-layout). If `rem` / `em` media queries depended on the `font-size` of the `html` element, you could create an infinite loop:

```css
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](https://bugs.webkit.org/show_bug.cgi?id=156684)). Since this breaks the "no layout-dependent selectors" CSS rule, we can actually [witness the infinite loop](https://danburzo.github.io/browser-summary/safari) 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](https://hacks.mozilla.org/2013/09/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](https://bugs.webkit.org/show_bug.cgi?id=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](https://danburzo.github.io/browser-summary/safari-zoom)!

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 <html> element's font size the way `rem`s are, we can, if that helps in any way, go ahead and adjust it:

```css
@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](https://medium.com/@vamptvo/pixels-vs-ems-users-do-change-font-size-5cfb20831773), 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](https://twitter.com/zcorpan) for corrections & guidance in navigating the W3C specs._ 

---

## Appendix: Deprecated media queries

[CSS Media Queries 4](https://www.w3.org/TR/2017/CR-mediaqueries-4-20170905/#mf-deprecated) 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](https://drafts.csswg.org/cssom-view-1/#web-exposed-screen-area), 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](https://danburzo.github.io/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()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia) method:

```js
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](https://drafts.csswg.org/cssom-view/#extensions-to-the-window-interface) 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](https://en.wikipedia.org/wiki/Bisection_method) to avoid making a gazillion queries:

```js
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`.