Page Transitions are becoming a standard part of screen-based user interfaces' design and development process. With the View Transitions API making its way into a majority of browsers, we must remember to consider accessibility requirements and build with them in mind.
# Only YOU can prevent Motion Fires
"Make it bounce!", "Make it fly!", "Set it on fire!" are becoming the new "Make my logo bigger". Animations have been bringing flat, intangible user interfaces (UIs) to life for years. Being part of the industry for almost 25 years, I remember the monotonous days typing away on ActionScript and lining up keyframes in Flash. Now, with the advent of nearly all major web browsers supporting the CSS View Transitions Spec, this comes easier than a cake in a bake shop.
The View Transitions API provides a mechanism for easily creating animated transitions between different DOM states while also updating the DOM contents in a single step.
Where once there were constraints that required us to carefully craft animations and page transitions, we can now easily add motion to our applications. Great news, because it gives us extra time to progressively (de)enhance applications for users with special needs.
# Vestibular Impairment
There are a subset of accessibility (a11y) considerations when designing and implementing animations and page transitions. A study published in the National Library of Medicine found that around 35% of adults over 40 years old experience balance dysfunction.
Vestibular Disorders affect the sensory system which helps keep our sense of balance and spatial orientation. The a11y Project briefly describes the affliction as such:
People with vestibular disorders have a problem with their inner ear. It affects their balance as well as their visual perception of their world around them.
With animations and motion now prevalent on the internet, imagine how it must feel ducking and shielding one's eyes from the endless, flaming torment of thousand-pixels wide fly-ins and bouncing elastic buttons.
Let's take a peek at the great responsibility we must embrace as all-powerful animation-slinging user interface engineers we have become.
# Reducing Motion for Vestibular Impairment
Using JavaScript's matchMedia
and CSS's @media
features, we can reduce or remove animations, transitions, and motion that could affect our users.
# tl;dr — The Goal
Below are before and after examples of this in action on dgrebb.com. The CSS, JavaScript, and Animated Image sections below contain solutions and examples to tailor for your flavor of web development.
# Before
# After
Removing global DOM manipulation, custom transitions, and reducing CSS property transition time, users are now able to browse through pages without a motion-heavy experience. For dgrebb.com, I've adjusted:
- Animated Images
- CSS Property and View Transitions
- CSS and SVG Animations
- Animated JavaScript DOM Manipulations
Let's take a look at images.
# Animated Images
A number of image formats support sequenced frames, perceived as "animations", including GIF, WebP, APNG, and the lesser-known FLIF, MNG, and AVIF. While animated images provide a medium to convey rich details about a complex scene or interaction, they are a jarring nuisance to some users. There is a way to enhance this experience with a11y in mind.
# Playing and Pausing Images
Browser APIs now include the Web Animations API. Unfortunately, sequenced images aren't supported. Including an animated image on a web page, they will play automatically, without a user option to pause.
Thanks to the fine folks at CSS Tricks, a simple copy, paste, and adjust solution is available, and gets the job done. In fact, this technique is in use at the top of this page!
By default, we'll display a still image and add a "play/pause" button, which swaps between the still frame and animation on click.
The best part — there is no need to reach for your JavaScript multitool! With a little CSS, the rarely-used details
and summary
elements get the job done:
<figure class="animated-image">
<div class="animation-player">
<img
src="https://imgtr.ee/images/2023/10/21/34cdee1935e9677e0defa5ae50ad73a4.jpeg"
alt="Still frame of motion sickness"
loading="lazy"
/>
<details>
<summary aria-label="Click to play the animation">
Play/Pause
</summary>
<div class="animation">
<img
src="https://media4.giphy.com/media/WS0I3awIzkDWQyClKF/giphy.gif"
alt="Animated {altText}}"
loading="lazy"
/>
</div>
</details>
</div>
<figcaption>Buckets of fun!</figcaption>
</figure>
With some positioning trickery and the details
element's open
attribute, we can display a still frame while in the open
attribute is not included on the details
element.
Note
The details
element's open
attribute — which displays the contained content — is truly boolean. It's either there or not. There is no open="false"
. Only open=""
.
These styles give an idea of the overall player and how it works:
.animated-image {
position: relative;
}
.animation-player > img {
z-index: 1;
}
.animated-image summary {
z-index: 2;
position: absolute;
top: 1rem;
left: 1rem;
background-color: rgba(0, 0, 0, 0.5);
color: white;
padding: .5rem 1rem;
border-radius: 1rem;
cursor: pointer;
}
/*
* @@@ the animated image @@@
* shown when `details[open]`
* positioned to cover the still image
*/
.animation img {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
/* @@@ play button @@@ */
summary {
.pause-icon {
display: none;
}
.play-icon {
display: inline;
}
}
/* @@@ pause button @@@ */
[open] summary {
&:hover {
opacity: 0.77;
}
.play-icon {
display: none;
}
.pause-icon {
display: inline;
}
}
To enable automatic playing of these images, simply add the open
attribute to details
in the default element state. Combining this with a prefers-reduced-motion
media query, the image can be set to play automatically for all users except those who prefers-reduced-motion: reduce
.
Play around with this example on JSFiddle, if you are so inclined. Let's look at CSS animation and transition properties next.
# CSS Transitions & Animations
Browsers set the prefers-reduced-motion
CSS media feature value based on a user's system and/or application motion reduction settings. By querying this, we can easily reduce the duration of CSS transitions and animation properties.
Start by setting transition and animation properties extremely low. This virtually removes motion from the page. An important note here, setting a duration protects functionality involving eventlisteners bound to the animationEnd
event, as an example.
Equally as important and future-proofing, we can also set View Transitions API "old" and "new" state animation duration.
@media (prefers-reduced-motion: reduce) {
*,
::before,
::after,
::view-transition-old(*),
::view-transition-new(*) {
animation-duration: 0.001s !important;
animation-iteration-count: 1 !important;
transition-delay: 0.001s !important;
transition-duration: 0.001s !important;
}
}
How far you choose to go wildcarding elements in your application or website requires some thought and reflection on your potential audience. If animation or transitions are a UI affordance, drawing your users' attention to important actions, think about increasing their duration—thus slowing them down—as another option.
Important
Update: Keep this snippet around for situations where other media queries (namely breakpoints) override CSS specificity.
Be mindful that dynamically loaded CSS via JavaScript can sometimes override !important
rules, especially in frameworks like Svelte and Astro. Also look out for any CSS transition
properties set with a :hover
, :focus,
:active, etc. selectors — they will need to be unset for
prefers-reduced-motion: reduce`.
@media screen and (prefers-reduced-motion: reduce) and (width >= 1px) {
*,
::before,
::after,
::view-transition-old(*),
::view-transition-new(*) {
animation-duration: 0.001s !important;
animation-iteration-count: 1 !important;
transition-delay: 0.001s !important;
transition-duration: 0.001s !important;
scroll-behavior: auto !important;
}
}
# JavaScript matchMedia
JavaScript provides a handy window
method — matchMedia()
, which takes a parameter of mediaQueryString
, much like CSS mediaQuery selectors work. @media (prefers-reduced-motion: reduce) {}
becomes window.matchMedia('(prefers-reduced-motion: reduce)').matches;
, which returns true
or false
based on a user's motion preferences.
Let's use this to enhance our JavaScript-assisted page and element transitions, and turn them off if reduced motion is set in the browser.
# Svelte Directives
For built-in Svelte transitions via import {fade} from 'svelte/transition'
, we can exit a transition before it starts via a matchMedia
helper. Let's create and add a custom move
function to our transition
in
and out
directives.
<script>
import { fade, fly } from 'svelte/transition';
import { circInOut } from 'svelte/easing';
function prefersReducedMotion() {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
let motion = !prefersReducedMotion();
let show = false;
function move(node, options) {
if (motion === false) return false;
return options.fn(node, options);
}
</script>
<button on:click={() => { show = !show }} on:keydown={() => { show = !show }}>
Show Me!
</button>
{#if show}
<h3
in:move={{ fn: fly, x: -333, duration: 500, easing: circInOut }}
out:move={{ fn: fade, duration: 500 }}
>
Motion Intensifies!
</h3>
{/if}
Here, we pass the element node and transition options into move
. move
then determines if a local motion
variable is truthy, and if so, proceeds with the transition
function and options
we pass from the element being transitioned. If motion
has been set to false
, we exit the helper and never apply motion or animated effects.
# Demo and Example
I've provided a Svelte REPL (Read-Eval-Print-Loop) of this solution on svelte.dev. Using Windows, macOS, or browser preferences, the effects of "Reduced Motion" can be explored there.
Because this example is a sandboxed environment, the page must be refreshed to see a system settings change. REPLs only have access to your browser's queryable attributes on load. Try it out and experience how easy it is to (progressively) reduce (or add!) motion.
This website itself will semi-immediately reflect system settings changes. However, be aware that mount
is when transitions become aware of their next out
transition. For me, this is acceptable in supporting a user's choice to reduce motion in the middle of navigating a site.
# Custom Svelte Transitions
For custom transitions, apply the same logic. Exit a transition before it starts if'(prefers-reduced-motion: reduce)'
is set. We make use of the same prefersReducedMotion
helper function within our custom transitions like so:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
On lines 19, 34, and 38, we return from the transition, rather than manipulating the DOM, which would apply fade in and out animations
Because I use custom transition functions, and eventListener
s on onintrostart
, onintroend
, onoutrostart
, and onoutroend
, this is the way motion is reduced on dgrebb.com. Because matchMedia
is a window
method, we need to either a) call the helper inside of our transition functions, or b) import and call onMount()
to set a local variable on page load.
Motion preferences can also be captured in global application state depending on preference and the complexity of your transition ecosystem. Another option for a more global approach; one could include a motion toggle or checkbox in the UI, or include the same in a user account settings page.
I prefer supporting changes to motion preferences on the fly, so the small performance hit and repeated code are acceptable (to me).
# In Closing
Remember to keep your empathetic wits about you when it comes to animation and page or element-level transitions. The last thing we need is a monitor thrown from a balcony, or set on fire, from any frustration or serious illness our beloved users might experience.