JoshWComeau

The Surprising Truth About Pixels and Accessibility

Filed under
CSS
on
in
May 17th, 2022.
May 2022.
Last updated
on
in
November 17th, 2024.
Nov 2024.
Introduction
Should I use pixels or ems/rems?!

This is a question I hear a lot. Often with a dollop of anxiety or frustration behind the words. 😅

It's an emotionally-charged question because there are a lot of conflicting opinions out there, and it can be overwhelming. Maybe you've heard that rems are better for accessibility. Or maybe you've heard that the problem is fixed and pixels are fine?

The truth is, if you want to build the most-accessible product possible, you need to use both pixels and ems/rems. It's not an either/or situation. There are circumstances where rems are more accessible, and other circumstances where pixels are more accessible.

So, here's what we're going to do in this tutorial:

  1. We'll briefly cover how each unit works, to make sure we're all building on the same solid foundation.
  2. We'll look at what the accessibility considerations are, and how each unit can affect these considerations.
  3. We'll build a mental model we can use to help us decide which unit to use in any scenario.
  4. I'll share my favourite tips and tricks for converting between units.

By the end, you'll be able to use your intuition to be able to figure out which unit to use in any scenario. 😄

Link to this headingUnit summaries

Link to this headingPixels

The most popular unit for anything size-related is the px unit, short for “pixel”:

.box {
  width: 1000px;
  margin-top: 32px;
  padding: 8px;
}

In theory, 1px is equal to a single dot in a computer monitor or phone screen. They're the least-abstract unit we have in CSS, the closest "to the metal". As a result, they tend to feel pretty intuitive.

Link to this headingEms

The em unit is an interesting fellow. It's a relative unit, based on the element's calculated font size.

Fiddle with these sliders to see what I mean:

16px
1.5em

Some words and things.

16 × 1.5 = 24px

Essentially, em is a ratio. If our paragraph has a bottom margin of 1.5em, we're saying that it should be 1.5x the font size. This allows us to “anchor” one value to another, so that they scale proportionally.

Here's a silly example. Each word in the following sentence uses a smaller em value, giving the impression of a sentence fading into the distance. Try tweaking the paragraph's font-size, and notice how everything “zooms in”:

Code Playground

HTML

Result

Link to this headingRems

It's old news now, but there was a time when the rem unit was a shiny new addition to the CSS language.

It was introduced because there's a common frustrating issue with the em unit: it compounds.

For example, consider the following snippet:

<style>
  main {
    font-size: 1.125em;
  }
  article {
    font-size: 0.9em;
  }
  p.intro {
    font-size: 1.25em;
  }
</style>

<main>
  <article>
    <p class="intro">
      What size is this text?
    </p>
  </article>
</main>

How large, in pixels, is that .intro paragraph font?

To figure it out, we have to multiply each ratio. The root font size is 16px by default, and so the equation is 16 × 1.125 × 0.9 × 1.25. The answer is 20.25 pixels.

What? Why?? This happens because font size is inheritable. The paragraph has a font size of 1.25em, which means “1.25x the current font size”. But what is the current font size? Well, it gets inherited from the parent: 0.9em. And so it's 1.25x the parent, which is 0.9x its parent, which is 1.125x its parent.

Essentially, we need to multiply every em value in the tree until we either hit a "fixed" value (using pixels), or we make it all the way to the top of the tree. This is exactly as gnarly as it sounds. 😬

To solve this problem, the CSS language designers created the rem unit. It stands for “Root EM”.

The rem unit is like the em unit, except it's always a multiple of the font size on the root node, the <html> element. It ignores any inherited font sizes, and always calculates based on the top-level node.

Documents have a default font size of 16px, which means that 1rem has a “native” value of 16px.This value, however, is user-configurable! More on this shortly

We can re-define the value of 1rem by changing the font-size on the root node:

16px

Hello World

This is a paragraph with some words and things.

We can do this, but we shouldn't.

In order to understand why, we need to talk about accessibility.

Link to this headingAccessibility considerations

The main accessibility consideration when it comes to pixel-vs-em/rem is vision. We want people with limited vision to be able to comfortably read the sentences and paragraphs on our websites and web applications.

There are a few ways that folks with limited vision can increase the size of text.

One method is to use the browser's zoom functionality. The standard keyboard shortcut for this is + on MacOS, ctrl + on Windows/Linux.

I'll call this method zooming in this tutorial.

The Web Content Accessibility Guidelines (WCAG) state that in order to be accessible, a site should be usable at 200% zoom(opens in new tab). I've heard from accessibility advocates that this number is really a minimum, and that many folks with vision disorders often crank much higher than that.

Finally, there's another method, one that fewer developers know about. We can also increase the default font size in our browser settings:

I'll call this method font scaling in this tutorial.

Font scaling works by re-defining the “baseline” font size, the default font size that all relative units will be based on (rem, em, %).

Remember earlier, when we said that 1rem was equal to 16px? That's only true if the user hasn't touched their default font size! If they boost their default font size to 32px, each rem will now be 32px instead of 16.

Essentially, you can think of font scaling as changing the definition of 1 rem.

Here's where we hit our first accessibility snag. When we use a pixel value for a font-size on the page, it will no longer be affected by the user's chosen default font size.

100%
Font unit:

This is a paragraph with some words and things.

This is why we should use relative units like rem and em for text size. It gives the user the ability to redefine their value, to suit their needs.

Now, the picture isn't as bleak as it used to be, thanks to browser zooming.

When the user zooms in or out, everything gets bigger. It essentially applies a multiple to every unit, including pixels. It affects everything except viewport units (like vw and vh). This has been the case for many years now, across all major browsers.

So, if users can always zoom to increase their font size, do we really need to worry about supporting font scaling as well? Isn't one option good enough?

The problem is that zoom is really intended to be used on a site-by-site basis. Someone might have to manually tinker and fuss with the zoom every time they visit a new site. Wouldn't it be better if they could set a baseline font size, one that is large enough for them to read comfortably, and have that size be universally respected?

(Let's also keep in mind that not everyone can trigger a keyboard shortcut easily. A few years ago, I suffered a nerve injury that left me unable to use a keyboard. I interacted with the computer using dictation and eye-tracking. Suddenly, each “keystroke” became a lot more taxing!)

As a general rule, we should give the user as much control as possible, and we should never disable or block their settings from working. For this reason, it's very important to use a relative unit like rem for typography.

Link to this headingStrategic unit deployment

Alright, so you might be thinking: if the rem unit is respected by both zooming and font-scaling, shouldn't I always use rem values? Why would I ever use pixels?

Well, let's see what happens when we use rem values for padding:

100%
Padding unit:

This is a paragraph containing many words in a specific, intentional order.

Remember that rem values scale with the user's default font size. This is a good thing when it comes to typography. Is it a good thing when it comes to other stuff, though? Do I actually want everything to scale with font size?

There's an implicit trade-off when it comes to text size. The larger the text is, the fewer characters can fit on each line. When the user cranks up the text by 250%, we can only fit a few words per line.

When we use rem values for horizontal padding, though we amplify this negative side-effect! We're reducing the amount of usable space, further restricting how many words can fit on each line.

This is bad
because
paragraphs
like this one
with only a
few words per
line are
unpleasant to
read.

Similarly, how about border widths? It doesn't really make sense for a border to become thicker as the user scales up their preferred text size, does it?

This is why we want to use these units strategically. When picking between pixels and rems, here's the question you should be asking yourself:

Should this value scale up as the user increases their browser's default font size?

This question is the root of the mental model I use. If the value should increase with the default font size, I use rem. Otherwise, I use px.

That said, the answer to this question isn't always obvious. Let's look at some examples.

Link to this headingMedia queries

Should we use pixels or rems for our media query values?

/* Should we do this: */
@media (min-width: 800px) {
}

/* …Or this: */
@media (min-width: 50rem) {
}

It's probably not obvious what the distinction is here, so let's break it down.

Suppose a user sets their default text size to 32px, double the standard text size. This means that 50rem will now be equal to 1600px instead of 800px.

By sliding the breakpoint up like this, it means that the user will see the mobile layout until their window is at least 1600px wide. If they're on a laptop, it's very likely they'll see the mobile layout instead of the desktop layout.

At first, I thought this seemed like a bad thing. They're not actually a mobile user, so why would we show them the mobile layout??

I've come to realize, however, that we usually do want to use rems for media queries.

Let's look at a real-world example.

On my course platform, I have a left-hand navigation list, with the content shown on the right:

Screenshot of a lesson in a course platform, with left-hand navigation

On smaller screens, I want to maximize the amount of space for the content, and so the navigation becomes toggleable:

Let's see what happens when the user visits with a 32px default font size, using both pixels and rem media queries:

Pixel Media Query

The desktop layout is used, with HUGE text, and not much space for it

Rem Media Query

The mobile layout is used, with HUGE text, and plenty of space for it

The left-hand navigation uses a rem-based width, in order to prevent longer lesson names from line-wrapping if the user cranks up their default font size. When we use a pixel-based media query, this means that the sidebar takes up most of the window, on smaller laptop screens!

When we use a rem-based media query, however, we drop back down to the “mobile” layout. As a result, the content becomes much more readable, and the experience is much improved.

We're so used to thinking of media queries in terms of mobile/tablet/desktop, but I think it's more helpful to think in terms of available space.

A mobile user has less available space than a desktop user, and so we design layouts that are optimized for that amount of space. Similarly, when someone cranks up their default font size, they reduce the amount of available space, and so they should probably receive the same optimizations.

And so, I recommend using rems for media queries. It means that users who crank up their default font size will see the “mobile” view even on a desktop computer, but this is generally a better user experience than a super-crowded desktop layout with huge text.

Link to this headingVertical margins

Let's look at another scenario. Vertical margins:

100%
Margin unit:

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text since the 1500s.

History

It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

For more information, visit the Lorem Ipsum museum.

Vertical margins on text (assuming we're working in a horizontally-written language like English) are typically used to improve its readability. We add extra space between paragraphs so that we can quickly tell where one paragraph ends and the next one begins.Interestingly, the web is somewhat unique in terms of paragraph spacing. Print media, like books, indent the first line of each new paragraph, without adding any additional vertical space between paragraphs.

This space has a “functional” purpose when it comes to text. We aren't using it aesthetically.

For these reasons, I think it does make sense to scale these margins with the user's chosen root font size.

Link to this headingWidths and heights

Alright, let's consider one more scenario. Here we have a button with a fixed width:

100%
Width unit:
Random Button
225px

So, we know that the button's font-size should be set in rems… but what about its width?

There's a really interesting trade-off here:

  • If we set the width to be 240px, the button won't grow with font size, leading to line-wrapping and a taller button.
  • If we set the width to be 15rem, the button will grow wider along with the font size.

Which approach is best? Well, it depends on the circumstances!

In most cases, I think it makes more sense to use rems. Multi-line buttons look a bit funny to me. By using rems, we preserve the button’s proportions / aspect ratio.

In some cases, though, pixels might be the better option. Maybe if you have a very specific layout in mind, and vertical space is more plentiful than horizontal space.

Link to this headingTest your intuition

Alright, so we've learned that rem values should be used when we want to scale a value with the user's default font size.

What if it isn't obvious which option is best, though? Like with the button width?

The best thing to do in these cases is to test it. Change your browser's default font size to 32px or 48px, and see how your app feels. Try using pixels, and then try using rems. Which option produces the best user experience, the most readable content?

Over time, you'll develop a stronger and stronger intuition, as you see for yourself what to do in specific circumstances.

Not sure how to change your browser's default font size? Here's the documentation for the most commonly-used browsers:

If your browser isn't listed here, a quick Google search should turn it up!

Link to this headingQuick tricks vs. mental models

I have a philosophy when it comes to learning: It's better to build an intuition than it is to rely on rote practice and memorization.

This blog post could have been a quick list of rules: “Use pixels for X, use rems for Y”. But how useful would it actually have been?

The truth is, the real world is messy and complicated. No set of rules can possibly be comprehensive enough to cover every possible scenario. Even after writing CSS for 15 years, I still find myself facing novel layout challenges all the time!

When we focus on building an intuition, we don't need to memorize rules. We can rely on our mental model to come up with the right answer. It's wayyy more practical.

And yet, most of us learn from “quick tricks”. We pick up an interesting nugget on Twitter. We memorize a lil’ snippet to center a div or apply a flexible grid. And, inevitably, we hit snags where the snippet doesn't work as we expect, and we have no idea why.

I think this is why so many developers dislike writing CSS. We have a patchy mental model, and those holes make the language feel brittle and unpredictable, like a house of cards that is always on the verge of collapse.

When we focus on building an intuition, on learning how CSS really works, the language becomes a joy to use. I used to find CSS frustrating, but now, it's one of my favourite parts of web development. I love writing CSS.

I wanted to share this joy, and so I quit my job and spent a year building a comprehensive self-paced online course. It's called CSS for JavaScript Developers(opens in new tab).

This course takes the approach we've used in this tutorial and applies it to the entire CSS language. Using interactive demos and live-editable code snippets, we explore how the language works, and how you can build an intuition you can use to implement any layout. Not just the ones we cover explicitly.

I built a custom course platform from scratch, using the same technology stack as my blog. But it's so much more. It includes tons of bite-sized videos, exercises, real-world-inspired projects, and even a handful of mini-games. ✨

It's specifically built for JavaScript developers, folks who use a component-based framework like React or Vue. In addition to core language concepts, we also explore things like how to build a component library from scratch.

If you're sick of not understanding how CSS works, this course is for you. 💖

Learn more here: https://css-for-js.dev/(opens in new tab)

Banner with the headline “Stop wrestling with CSS”. Sub-heading: “The all-new interactive learning experiecne designed to help JavaScript developers become confident with CSS”

Link to this headingBonus: Rem quality of life

Alright, so as we've seen, there are plenty of cases where we need to use rem values for best results.

Unfortunately, this unit can often be pretty frustrating to work with. It's not easy to do the conversion math in our heads. And we wind up with a lot of decimals:

  • 14px 0.875rem
  • 15px 0.9375rem
  • 16px 1rem
  • 17px 1.0625rem
  • 18px 1.125rem
  • 19px 1.1875rem
  • 20px 1.25rem
  • 21px 1.3125rem

Before you go memorize this list, let's look at some of the things we can do to improve the experience of working with rems.

Link to this headingThe 62.5% trick

Let's start with one of the most common options I've seen shared online.

Here's what it looks like:

html {
  font-size: 62.5%;
}

p {
  /* Equivalent to 18px */
  font-size: 1.8rem;
}
h3 {
  /* Equivalent to 21px */
  font-size: 2.1rem;
}

The idea is that we're scaling down the root font size so that each rem unit is equal to 10px instead of 16px.

People like this solution because the math becomes way easier. To get the rem equivalent of 18px, you move the decimal (1.8rem) instead of having to divide 18 by 16 (1.125rem).

But, honestly, I don't recommend this approach. There are a couple of reasons.

First, It can break compatibility with third-party packages. If you use a tooltip library that uses rem-based font sizes, text in those tooltips is going to be 37.5% smaller than it should be! Similarly, it can break browser extensions the end user has.

There's a baseline assumption on the web that 1rem will produce readable text. I don't wanna mess with that assumption.

Also, there are significant migration challenges to this approach. There's no reasonable way to “incrementally adopt” itWell, you could, but then the definition of 1rem wouldn't be consistent across the site/app, which sounds like a nightmare. You'll need to update every declaration that uses rem units across the app. Plus, you'll need to convince all your teammates that it's worth the trouble. Logistically, I'm not sure how realistic it is for most teams.

Let's look at some alternative options.

Link to this headingCalculated values

The calc CSS function can be used to translate pixel values to rems:

p {
  /* Produces 1.125rem. Equivalent to 18px */
  font-size: calc(18rem / 16);
}
h3 {
  /* Produces 1.3125rem. Equivalent to 21px */
  font-size: calc(21rem / 16);
}
h2 {
  /* Produces 1.5rem. Equivalent to 24px */
  font-size: calc(24rem / 16);
}
h1 {
  /* Produces 2rem. Equivalent to 32px */
  font-size: calc(32rem / 16);
}

(Thanks to Cahnory(opens in new tab) for improving on my original idea!)

Pretty cool, right? We can do the math right there inside the CSS declaration, and calc will spit out the correct answer.

This is a viable approach, but it's a bit of a mouthful. It's a lot of typing every time you want to use a rem value.

Let's look at one more approach.

Link to this headingLeveraging CSS variables

This is my favourite option. Here's what it looks like:

html {
  --14px: 0.875rem;
  --15px: 0.9375rem;
  --16px: 1rem;
  --17px: 1.0625rem;
  --18px: 1.125rem;
  --19px: 1.1875rem;
  --20px: 1.25rem;
  --21px: 1.3125rem;
}

h1 {
  font-size: var(--21px);
}

We can do all the calculations once, and use CSS variables to store those options. When we need to use them, it's almost as easy as typing pixel values, but fully accessible! ✨

It's a bit unconventional to start CSS variables with a number like this, but it's compliant with the spec, and appears to work across all major browsers.

If you use a design system with a spacing scale, we can use this same trick:

html {
  --font-size-xs: 0.75rem;
  --font-size-sm: 0.875rem;
  --font-size-md: 1rem;
  --font-size-lg: 1.125rem;
  --font-size-xl: 1.3125rem;
  --font-size-2xl: 1.5rem;
  --font-size-3xl: 2.652rem;
  --font-size-4xl: 4rem;
}

CSS variables are absolutely delightful. We explore a bunch of cool things we can do with them in CSS for JavaScript Developers(opens in new tab)!

Ultimately, all of these methods will work. I certainly have my preferences, but the important thing is the end user experience. As long as they can adjust the size of all text on the page, you're doing it right. 💯

Last updated on

November 17th, 2024

# of hits