6

The Surprising Truth About Pixels and Accessibility

 1 year ago
source link: https://www.joshwcomeau.com/css/surprising-truth-about-pixels-and-accessibility/?ref=sidebar
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
“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. 😄

Unit summaries

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.

Hardware vs. software pixels

So, the px unit is a bit of a lie. It doesn't actually map neatly onto hardware pixels.

If you look at a modern display under a microscope, you'll realize that they aren't made up of crisp little R/G/B rectangles anymore. Here are close-up shots of the screens on the Apple Watch and Apple iPhone:

pixel-closeup-apple-watch.jpg
pixel-closeup-iphone.jpg

(Sources: Apple Watch and iPhone.)

Even before manufacturers started getting creative with pixel grids, there was still a distinction between the physical pixels in a screen and the software pixels we write in CSS. Every time a user changes their screen's resolution or zooms in, they're changing how software pixels map onto hardware pixels.

That said, none of this should really affect how we feel about the px unit. It's still the most concrete unit we have!

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:

Some words and things.

16 × 1.5 = 24px
.some-paragraph {
font-size: 16px;
margin-bottom: 1.5em;
font-size:16px
margin-bottom:1.5em

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

<style>
/* Change me! */
font-size: 24px;
</style>
<p>
<span style="font-size: 1em">
</span>
<span style="font-size: 0.8em">
sentence
</span>
<span style="font-size: 0.64em">
</span>
<span style="font-size: 0.5em">
quieter
</span>
<span style="font-size: 0.4em">
</span>
<span style="font-size: 0.32em">
quieter
</span>
</p>

Result

Enable ‘tab’ key

Note: To make it easier to understand how the em unit works, we're using pixel-based font sizes here. As we'll learn shortly, however, this is a bad idea. Please don't do this in real applications!

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.

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

Hello World

This is a paragraph with some words and things.

html {
font-size: 16px;
font-size: 2rem;
font-size:16px

We can do this, but we shouldn't.

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

Accessibility 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. 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.

This is a paragraph with some words and things.

font-size: 1rem;
Default font size:100%
Font unit:
rempx

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.

Strategic 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:

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

.wrapper {
font-size: 1rem;
padding: 32px;
Default font size:100%
Padding unit:
pxrem

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.

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:

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

As we increase the size of the text, the left-hand navigation gets wider and wider (because it uses a rem-based width). As a result, the main content area gets squeezed smaller and smaller.

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.

Vertical margins

Let's look at another scenario. Vertical margins:

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.

margin-top: 32px;
p, h2 {
margin-bottom: 16px;
Default font size:100%
Margin unit:
pxrem

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.

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.

A rare opportunity for the “em” unit

When I need a relative unit, I almost always reach for rem. It's much simpler and more predictable than em, for the “compounding” issues discussed earlier.

That said, the em unit works particularly well when it comes to margins on headings and paragraphs.

Widths and heights

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

Random Button
225px
.button {
font-size: 1.25rem;
width: 240px;
max-width: 100%;
Default font size:100%
Width unit:
pxrem

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. This preserves the button's proportions, its aesthetics. And it reduces the risk of an overflow, if the button has a particularly long word.

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.

Constraints

In general, we need to be really careful when setting fixed widths and heights.

In the example above, setting width: 15rem will, in many cases, break mobile layouts, since it may produce a value too large for its container when the user cranks up their default font size!

We can often mitigate this by clamping it to a maximum of 100%:

.button {
max-width: 100%;

Similarly, when it comes to heights, we often want to use min-height instead of height. This allows the container to grow as tall as it needs, in order to contain its children. This becomes important when a user scales up their font size, since the text will wind up wrapping onto more lines.

Test 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!

Quick 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.

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/

Bonus: 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.

The 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%;
/* Equivalent to 18px */
font-size: 1.8rem;
/* 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 few reasons.

The first is that it can be risky. The default text size on the page is now equivalent to 10px. If you forget to apply a font-size somewhere, the text is going to be very tiny.

Also, 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 mess with any browser extensions the end user has.

Finally, there are significant migration challenges to this approach. There's no reasonable way to “incrementally adopt” it. 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.

Calculated values

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



/* Produces 1.125rem. Equivalent to 18px */
font-size: calc(18 / 16 * 1rem);
/* Produces 1.3125rem. Equivalent to 21px */
font-size: calc(21 / 16 * 1rem);
/* Produces 1.5rem. Equivalent to 24px */
font-size: calc(24 / 16 * 1rem);
/* Produces 2rem. Equivalent to 32px */
font-size: calc(32 / 16 * 1rem);

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.

Leveraging 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;
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;

One more bit of shameless self-promotion before I wrap up: In CSS for JavaScript Developers, we explore many of the ideas discussed here, including CSS variables, calc, design systems, and oh so much more. ✨

Last Updated

May 17th, 2022


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK