Making sense of units in CSS Media Queries (in the year 2019)

· 3054 words · Tweet this

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 most, while the max- stands for at least.

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 rems in your styles from the meaning of rems 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 rems 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 rems, fewer ems. 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 rems.

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 ems 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 rems 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:

In addition, I’ve used Browserstack to check:

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.

This article is cross-posted to dev.to, where you can leave comments and suggestions!

Earlier: A CSS-only layout debugger