Writing even more CSS with Accessibility in Mind, Part 2: Respecting user preferences
posted on
In the first article of this series, I explained how important progressive enhancement is for web accessibility. Building websites layer by layer allows for a cleaner separation of concerns and more resilient experiences. This second article is about user preferences and how to respect them when writing CSS.
In this series
This series of articles covers 4 major topics:
- Progressive enhancement
- User preferences (this article)
- CSS and semantics (coming soon)
- Improving accessibility with CSS (coming soon)
Respecting user preferences
Operating systems and browsers provide users with options to customize their browsing experience, and it’s our job to respect these preferences in our style sheets. In the following chapter, I’ll give you examples of how we can build designs according to our ideas while still respecting user preferences.
Font size
A fundamental thing we should do is respect our users’ preferred font size for running text.
Base font size
The default font size in most browsers is 16px
but it can be changed to a lower or higher value in the browser preferences.
If an user sets the default font size to 20px
, we should make sure that the size of running text doesn’t get lower than that on our website. We can do that by using relative units instead of absolute units. The problem with absolute units is that if we set the font size to 18px
on the <body>
, it overwrites the 20px
our user has chosen as their default, and that wouldn’t be nice of us.
body {
/* Don’t use px for font sizes (...in most cases) */
font-size: 18px;
}
Video demo:
If we use a relative unit like rem
, instead, we can still define our preferred size while respecting user preferences. 1rem
is relative to the root font size, as already mentioned, 16px
in most browsers. This means that the the size of 1rem
changes with the default font size in the browser.
If we want to use this unit, we have to convert px
to rem
. We can do that by taking the target value (18px
) and dividing it by the default root value (16px
):
The result is the rem
value. If we use it instead of the absolute pixel value, the font size for users with standard settings will still equal 18px
, but the font size for users who prefer a larger base font size like 20px
will be 22.5px
(1rem = 20px, 1.125rem = 22.5px).
body {
font-size: 1.125rem; /* 16 * 1.125 = 18px */
}
Video demo:
It makes sense to use relative units like rem
, em
, or %
because they’re relative to a parents font size when used with the font-size
property. Other relative units like vh
or vw
are not suitable because they’re relative to the viewport. Even combinations of units like calc(2vw + 0.5rem)
should be treated with caution because they might lead to unexpected results. Read Responsive Type and Zoom by Adrian Roselli for details.
Line lengths
I like to constrain the width of content container elements to improve readability by ensuring that lines don’t exceed a certain length. I use the ch
(character) unit for that. 1ch
is as wide as the glyph 0 (zero) in the respective font and size. If we set the max-width
to 80ch
, only approximately 80 characters will fit each line. A pleasant side effect is that when users change the default font size, the width of the content container grows with the size of the font. The relative unit ch
makes sure that there’s always a pleasant font size to line length ratio, no matter how large the text is.
.content {
max-width: 80ch;
}
Video demo:
Motion and animation
Animations on the web are sometimes annoying, but they may also cause nausea, dizziness, and headaches in some users. For people with vestibular disorders it may even cause pain and make them feel so bad that they have to stop using the computer, needing time to recover. Earlier this year I read “Accessibility for Vestibular Disorders: How My Temporary Disability Changed My Perspective” by Facundo Corradini and it blew my mind because it was the first time that I read about the negative side effects of animations on the web by someone who experienced it first-hand. Facundo is a developer, and he describes which patterns and effects were especially bad and made him feel dizzy and sick.
Really, there are no words to describe just how bad a simple parallax effect, scrolljacking, or even
background-attachment: fixed
would make me feel. I would rather jump on one of those 20-G centrifuges astronauts use than look at a website with parallax scrolling.
He also describes how distracting animations can be when you’re having a hard time focussing itself.
The extreme, conscious, focused effort it took to read would make it such that anything moving on the screen would instantly break my focus, and force me to start the paragraph all over. And I mean anything.
I can highly recommend this article, it outlines well how important it is to use animation on the web cautiously. Now let’s see how we can do that.
Reduce or remove motion if users prefer reduced motion
Some operating systems allow users to reduce motion, and we can react to that in CSS by using the prefers-reduced-motion
media feature. If our users prefer less motion on the screen, we should respect that and serve CSS without or with fewer animations and transitions.
Here’s an example: If you don’t have any preference for reduced motion, you should see someone moonwalking from one end of the article to the other. If you do, you should see someone moonwalking in place.
Inspired by Marquee Jackson by Lasse Diercks.
<img
src="https://media.giphy.com/media/EDZP0UCtxiRQQ/giphy.gif"
alt="Person doing the moon walk."
/>
/* Define keyframe animation */
@keyframes walk {
0% {
transform: translateX(100vw);
}
}
/* Apply animation */
img {
animation: walk 10s linear infinite;
}
/* Remove animation if the user prefers reduced motion */
@media (prefers-reduced-motion: reduce) {
img {
animation: none;
}
}
We could go one step further and replace the gif with a png by using a combination of the prefers-reduced-motion
media feature and the picture element.
<picture>
<source srcset="moonwalk.png" media="(prefers-reduced-motion: reduce)" />
<img src="moonwalk.gif" alt="Someone doing the moonwalk" />
</picture>
picture element and prefers-reduced-motion demo on CodePen
In this example I set the animation
property to none
, but you don’t always have to remove motion entirely. Eric W. Bailey points out that animation isn’t bad per se, because it may help users, especially people with cognitive disabilities, understand the relationship between seemingly disparate objects. Val Head outlines even more benefits of animations in “Designing Safer Web Animation For Motion Sensitivity” and “Designing With Reduced Motion For Motion Sensitivities”.
Progressive enhancement applied to animation
It’s important to respect user preferences, but this approach, applying animation and conditionally removing it, isn’t perfect. The reduced motion media feature was introduced only a few years ago, which means that users of legacy browsers will see animations no matter what.
Most articles recommend something like this:
/* Apply animations and transitions */
img {
animation: walk 10s linear infinite;
}
div {
transition: transform 1s ease-in;
}
/* Select all elements in the page and reduce or remove motion. */
/* Important note: This has no effect in browsers that don’t support prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
}
That’s why Patrick H. Lauke recommends a defensive prefers-reduced-motion use where animations only run in browsers that support the media feature and only if they have not expressed a preference for reduced motion.
/* has no effect in browsers that don’t support prefers-reduced-motion */
@media (prefers-reduced-motion: no-preference) {
img {
animation: walk 10s linear infinite;
}
div {
transition: transform 1s ease-in;
}
}
I like this approach because it’s more sensitive towards users and it takes progressive enhancement one step further. We start with no animation and only add this layer of enhancement if the browser supports the media feature and the user has no preference for reduced motion.
Here’s an example of how I’ve used the media feature in a website I’ve recently built. We only animate scrolling to anchor links, if users have no preference for reduced motion.
@media (prefers-reduced-motion: no-preference) {
html {
scroll-behavior: smooth;
}
}
Smooth scrolling demo on CodePen
Please respect your user’s motion preferences in your style sheets.
Using this media query could spare someone from having to unnecessarily endure a tremendous amount of pain for simply having the curiosity to click on a link or scroll down a page.
Dark Mode
Another setting some operating systems provide is the ability to switch the default theme from one that uses light colors to one that uses dark colors (Dark Mode). That’s especially useful for people who want to reduce eye strain or for people who have certain photosensitive conditions.
The media feature prefers-color-scheme
allows us to react to these settings and provide users with a dark or light design accordingly.
body {
/* Dark text on light background */
color: #222222;
background-color: #ffffff;
}
@media (prefers-color-scheme: dark) {
body {
/* Light text on dark background */
color: #ffffff;
background-color: #222222;
}
}
I did some research and collected useful tips and tricks for working with Dark Mode.
1. Change the design according to user preference but allow users to change it back.
Some users may prefer a dark theme for the OS but light themes on the web. They should be able to pick their preferred theme. You can see how Cassie Evans does it on her website cassie.codes.
2. Try to avoid hard contrasts like #FFFFFF
text color on a #000000
background.
Use softer combinations like #EFEFEF
on #111111
, which are easier on the eyes.
3. Dim images by decreasing the opacity
.
Dark backgrounds may amplify the brightness of light images, dimming images can help with that. You can still show the original opacity on hover and focus.
img {
opacity: 0.8;
transition: opacity 0.5s ease-in-out;
}
a:hover img,
a:focus img {
opacity: 1;
}
4. Simply inverting colors might not be want you want.
Some articles recommend to simply invert colors and call it a day. While this might work on some sites, it doesn’t mean that it works well.
html,
body {
background-color: #fff;
}
body {
color: #222;
}
@media (prefers-color-scheme: dark) {
html {
/* Invert the colors */
filter: invert(100%);
}
img {
filter: invert(100%); /* Invert the colors in images back to normal */
}
}
Dark Mode with filter: invert(100%) on CodePen
The filter
property and invert
function reverts colors in a page.
That’s not a best practice because just like you’ve picked the right combination of colors for your default theme carefully, you want to hand-pick the colors for your dark theme, too. However, on a component level filter: invert(100%)
might work well, so I’d keep it in my tool set.
5. Use the currentColor
keyword in SVGs for fill
or stroke
properties instead of absolute values.
In this simplified example you can see that the fill color of the SVG in the button is always the same as the text color of the button.
<button>
Sign up
<svg height="50" width="50" focusable="false">
<circle cx="25" cy="25" r="20">
</svg>
</button>
button {
color: #f00;
}
circle {
fill: #f00;
}
button:hover {
color: #0f0;
}
button:hover circle {
fill: #0f0;
}
@media (prefers-color-scheme: dark) {
button {
color: #00f;
}
circle {
fill: #00f;
}
button:hover {
color: #f0f;
}
button:hover circle {
fill: #f0f;
}
}
It works but the code is too verbose. We can reduce the number of lines and make it more dynamic by using the currentColor
keyword. currentColor
is relative to the color
property of the element or its parent element, it changes with the color on hover, focus, Dark Mode, etc.
button {
color: #f00;
}
button:hover {
color: #0f0;
}
circle {
fill: currentColor;
}
@media (prefers-color-scheme: dark) {
button {
color: #00f;
}
button:hover {
color: #f0f;
}
}
For the sake of completeness, here’s an example that uses custom properties and currentColor
.
:root {
--color: #f00;
--color_hover: #0f0;
}
@media (prefers-color-scheme: dark) {
:root {
--color: #00f;
--color_hover: #f0f;
}
}
button {
color: var(--color);
}
button:hover {
color: var(--color_hover);
}
circle {
fill: currentColor;
}
6. Swapping images
CSS has its limitations when it comes to swaping colors or manipulating colors in images. Sometimes its necessary to swap entire images. I found this great solution on Nando Vieras blog where he uses the picture
element and media queries to serve the right image.
<picture>
<source srcset="dark_logo.jpg" media="(prefers-color-scheme: dark)" />
<img src="light_logo.jpg" alt="Homepage" />
</picture>
Switch images in Dark Mode demo on CodePen
Users change browser or OS settings to improve their experiences for a reason. Please respect these decisions by writing CSS that’s flexible enough to respond to their preferences. Font size, motion, and dark colors settings are only examples, there’s even more to consider like High Contrast mode on Windows or inverted colors.
Resources
Dark Mode is a fantastic example for a feature that's essential for some and useful for all. If you want to add it to your website (yeah, I know, there’s no Dark Mode on my site. It’s a long story...), check these links. (Thanks, Max).
- Create a user controlled dark or light mode by Andy Bell
- Building Inclusive Toggle Buttons by Heydon Pickering
- Dark Mode UI: the definitive guide by Atharva Kulkarni
- Respecting User Preferences on the Web by Eric Eggert (video)
Recording
If you want to learn more about CSS and accessibility and you don’t want to wait for me to publish the other articles in this series, you can watch my talk about writing CSS with accessibility in mind at #ID24:
Thanks to Eric for helping me with this article.