You may be familiar with media queries to detect a user's viewport resolution, like the following:
@media (min-width: 60rem) {
/* Styles for viewports larger than 60rem */
}
But did you know that @media
- also known as one of the CSS at-rules - contains 24 total features to query against? Review the whole list of @media
features on MDN, and check the browser support table (about 19 have decent cross-browser support).
The two we'll be covering have to do with user preferences - as in, features that are determined by operating system settings.
prefers-color-scheme
prefers-reduced-motion
About prefers-reduced-motion
#As MDN states, this preference query "is used to detect if the user has requested that the system minimize the amount of non-essential motion it uses."
I've been a developer for nearly 15 years, and over that time I've seen a lot of trends. One with a significant amount of staying power was parallax animation, thanks to being boosted by big brands like Nike and Apple.
Parallax animation is a good example of a harmful type of motion for users with vestibular disorders. This is due to the foreground and background moving at different rates, which for some users can cause dizziness, nausea, and headaches. As Val Head notes in Designing Safer Web Animation for Motion Sensitivities, the negative effects for some users can last long after they've seen the animation.
With the use of prefers-reduced-motion
, we can take steps towards reducing the impact of potentially harmful animations.
Using prefers-reduced-motion
#There are two keywords for determining the state of a user's setting related to reduced motion that is surfaced via prefers-reduced-motion:
no-preference
- the default, which as Tatiana Mac suggests may mean a user is ok with motion or that they aren't aware an option exists to reduce motionreduce
- a user has explicitly set their OS system setting for a reduced motion experience
And here's an example of setting both types. You can adjust this setting via your OS, or in Chromium dev tools it can be emulated under the "Rendering" tab.
Demo of using prefers-reduced-motion
@media (prefers-reduced-motion: no-preference) {
.shake:hover {
animation: shake 400ms ease-in-out;
}
}
@media (prefers-reduced-motion: reduce) {
.shake {
/* !important due to overriding this site's rules to
remove transitions when meeting this condition */
transition: transform 180ms ease-in !important;
}
.shake:hover {
transform: translateX(.25rem);
}
}
@keyframes shake {
0%, 100% {
transform: translateX(0.5rem);
}
50% {
transform: translateX(-0.5rem);
}
}
.shake {
font-size: 1.35rem;
font-weight: bold;
}
Shake on hover if motion allowed
If you are using many animations, you can instead place them all in their own stylesheet and conditionally load it in via the media
attribute on the link
element:
<link rel="stylesheet" href="animations.css"
media="(prefers-reduced-motion: no-preference)">
Or, if you have only some minimal animations or transitions, you can borrow a technique popularized from Andy Bell's modern CSS reset. This fast-forwards animations and transitions when a user prefers reduced motion.
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
Why not completely remove them? Because there are times when a script may be listening for the transitionend
or animationend
events so it's safer to allow an imperceptible duration.
Handling prefers-reduced-motion with JavaScript
#There is also a JavaScript way to detect the value of prefers-reduced-motion
, which is beneficial if your animations are JS controlled.
In the following snippet, we're checking if prefers-reduced-motion: reduce
matches. If it does match, then prefersReducedMotion
will evaluate to true meaning the user prefers reduced motion.
const motionMQ = window.matchMedia('(prefers-reduced-motion: reduce)');
const prefersReducedMotion = motionMQ.matches;
We can then extend that to listen in case that preference changes so we can adapt the experience accordingly.
motionMQ.addEventListener('change', handleMotionChange);
In this demo, the text will be set according to your preference and update if you toggle your motion setting.
Detecting prefers-reduced-motion in JS
.mq-element { font-size: 1.35rem; }
const motionMQ = window.matchMedia('(prefers-reduced-motion: reduce)');
const updateMQ = (reduced) => {
const element = document.querySelector('.mq-element');
if (reduced) {
element.textContent = 'reduce';
} else {
element.textContent = 'no preference';
}
};
// Update on load
updateMQ(motionMQ.matches);
// Update on change
motionMQ.addEventListener('change', () => {
updateMQ(motionMQ.matches);
});
...
Additional resources on prefers-reduced-motion
#- Learn more about the related WCAG Success Criterion 2.3.3: Animation from interactions which gives more context into why it's important to reduce animations
- Eric Bailey gives a bit more history and a demo in his introduction to the reduced motion media query
- Val Head presenting Making Motion Inclusive at An Event Apart demonstrates how reduced motion doesn't mean no animations allowed
- Lindsey Kopacz walks through creating a toggle to allow your users to choose a reduced motion experience instead of only relying on the preference query
- Josh Comeau looks at using prefers-reduced-motion in React
Using prefers-color-scheme
#The second preference query you may have heard of since it is associated with providing a dark mode version of your site.
The prefers-color-scheme
media feature does in fact detect whether a user has requested a dark or light theme.
While this often gets bundled up by developers into a face-off similar to tabs vs. spaces, the matter of dark or light has a lot to do with accessibility. For example, I myself preferred a light theme for years until I learned I had astigmatism which was making dark themes uncomfortable due to the appearance of "halos" around the letters.
The options for this query are fairly self-explanatory:
light
dark
Keep in mind that these are not expected to be as polar as white vs. black, but more about increasing the use of light or dark colors particularly for large areas of text.
A nifty way to manage your themes is by using CSS custom properties (covered in Day 3). By choosing general-purpose names for your custom property colors and using them carefully throughout your stylesheets, you can flip their values to retheme your site when either dark or light is requested.
:root {
--theme-background: white;
--theme-text: #222;
}
@media (prefers-color-scheme: dark) {
:root {
--theme-background: #222;
--theme-text: white;
}
}
body {
background-color: var(--theme-background);
color: var(--theme-text);
}
The following demo shows a minimal example of this idea in action. Once again you can change your user settings or test this via dev tools as noted for the previous section.
Demo of theming with prefers-color-scheme
.site {
--theme: "Light";
--theme-background: lightblue;
--theme-text: #222;
--theme-inverse: #f9f9f9;
--theme-primary: blue;
background-color: var(--theme-background);
color: var(--theme-text);
padding: 1rem;
}
@media (prefers-color-scheme: dark) {
.site {
--theme: "Dark";
--theme-background: #222;
--theme-text: #f9f9f9;
--theme-inverse: #444;
--theme-primary: cyan;
}
}
.site article {
width: min(40ch, 100%);
margin: 0 auto;
padding: 1rem;
background-color: var(--theme-inverse);
}
.site article * {
margin: 0;
}
.site article * + * {
margin-top: 1em;
}
.site article h2::after {
content: " " var(--theme);
}
.site a:not([class]) {
color: var(--theme-primary);
}
.site-btn {
display: inline-block;
text-decoration: none;
background-color: var(--theme-primary);
color: var(--theme-inverse);
padding: 0.5em 0.75em;
border-radius: 0.25em;
}
Theme Demo
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Exercitationem amet magnam corrupti!
Magnam, a. Delectus consectetur hic at? Consequuntur maxime hic repellat numquam culpa.
ButtonHandling prefers-color-scheme with JavaScript
#We can detect and use the prefers-color-scheme
value in a nearly identical format to prefers-reduced-motion
.
This demo also includes a different option for managing color themes via CSS custom properties.
Detecting prefers-color-scheme in JS
.theme-element {
background: var(--background, white);
color: var(--color, black);
border: 1px solid;
padding: 2rem;
font-size: 1.35rem;
}
@media (prefers-color-scheme: dark) {
.theme-element {
--background: #222;
--color: white;
}
}
const themeMQ = window.matchMedia('(prefers-color-scheme: dark)');
const updateTheme = (dark) => {
const element = document.querySelector('.theme-element');
if (dark) {
element.textContent = 'dark theme';
} else {
element.textContent = 'light theme';
}
};
// Update on load
updateTheme(themeMQ.matches);
// Update on change
themeMQ.addEventListener('change', () => {
updateTheme(themeMQ.matches);
});
...
Additional resources on prefers-color-scheme
#- Accessibility expert Adrian Roselli reminds us that when building a theme toggle, there are really three options: light, dark, and inherit from system preferences
- Andy Bell walks through one way to make a toggle including saving a user's selection to
localStorage
A few posts on setting up dark and light themes:
- Michelle Barker covers the custom property way to switch themes
- Christopher Kirk-Nielsen offers a DRY approach for managing themes that is flexible for more than two options
- The Material Design guidelines are worth a glance for different things to consider when developing a dark theme