Disabling the animation on GIFs can be a bit tricky. While adding something like animation: none
with prefers-reduced-motion
-media query to a native HTML element can be straightforward, GIFs don't provide such an interface.
There are several strategies to handle pausing the animation on GIFs. One way would be to show a still image for those who prefer reduced motion. Another option for playing the animation on-demand is using a button to control the animation.
I initially saw the idea of the strategy I'm using in this demo in an article by Chris Coyier and wanted to try out to implement it in React. I also live-coded this solution in React Finland's Vodcast about accessibility. You can find the episode at the end of this post.
The first version we'll implement has a still image, and the second version will have a toggle button to play and pause the animation. It will be only shown for users who prefer reduced motion, meaning they have the setting turned on in their operating systems.
If you want to read a bit more about the whole prefers-reduced-motion,
and the reasons why someone might need it, I've written a post about that:
You Make My Head Spin - Reducing the Motion on Web
Eevis ・ Dec 13 '20
V1: Motionless Image for Users Who Prefer Reduced Motion
Okay, so before diving into the actual coding part, we need to do some preparations. We need the GIF and the first (or basically any) frame extracted from that GIF as a still image. There are many different services for extracting the frames out of GIFs out there on the internet. The GIF I'm using is from the Cat API.
For doing the conditional showing of the image, we'll use the picture
-element. MDN defines it with the following words:
The HTML
<picture>
element contains zero or more<source>
elements and one<img>
element to offer alternative versions of an image for different display/device scenarios.
So, in our case, we'll need to offer an alternative for those who prefer reduced motion. <source>
has an attribute called media,
which takes a media query, and if the condition on it is true, the picture element uses that source for the image.
Combining all this information, we could write something like this:
const Gif = () => (
<picture>
<source
srcSet="frame1.gif"
media="(prefers-reduced-motion)" />
<img
src="cat-sewing.gif"
alt="A cat sewing yellow-green
cloth with a sewing machine."
/>
</picture>
)
Note, that media="(prefers-reduced-motion)"
is a shorthand for media="(prefers-reduced-motion: reduce)"
, so both ways are fine.
Because we are dealing with images, an alternative text is essential. The <source>
element only determines the source of the picture; the alt-text given for the image is the same for every source and comes from the img
-element.
If you don't have the "reduce motion"-setting on and are using a chromium-based browser like Chrome or Edge, you can emulate the media feature. Here are the instructions:
Emulating "prefers-reduced-motion"
If you prefer video, I made a screen recording on emulating the prefers-reduced-motion.
V2: A Button to Play the Animation
Okay, showing a non-moving image for those preferring reduced motion is a good start, and as a default, it can prevent unpleasant and even painful situations. However, giving control to the user is always better. If they know what's coming, it is easier to get through it. Also, they can choose not to see the animation.
So, what do we need?
- A way to show the animated GIF for users with a preference for reduced motion
- A button to toggle the playing and pausing the GIF's animation
- To show that button only for those who prefer reduced motion
Let's tackle these requirements one by one.
Show the Animated GIF To Users With prefers-reduced-motion.
We'll continue from the previous example. As we are using the source
-element for conditionally displaying the still image, we can remove that attribute when the user wants to see the moving image. We need a boolean attribute to determine if we're going to show the moving or non-moving image.
Let's add a state called play.
We can toggle it later by changing the state's value. We'll also use that state value to show or remove the <source>
-element from the picture:
const Gif = () => {
const [play, setPlay] = useState(false)
return (
<picture>
{!play &&
<source
srcSet="frame1.gif"
media="(prefers-reduced-motion)" />
}
<img
src="cat-sewing.gif"
alt="A cat sewing yellow-green
cloth with a sewing machine."
/>
</picture>
)
}
Button to Play the Animation
The next thing we need is the button that toggles the value of the play
-state. We also want to show the user the correct text in the button to understand what the button will do.
const Gif = () => {
const [play, setPlay] = useState(false)
const handleToggle = () => setPlay(!setPlay)
const buttonText = play ? 'Pause' : 'Play'
return (
<div>
<button onClick={handleToggle}>{buttonText}</button>
<picture>
{!play &&
<source
srcSet="frame1.gif"
media="(prefers-reduced-motion)" />
}
<img
src="cat-sewing.gif"
alt="A cat sewing yellow-green
cloth with a sewing machine."
/>
</picture>
</div>
)
}
A note from the code and the play/pause button: in the live coding, I added aria-pressed
and aria-label
-attributes, but I'm leaving them out in this example. The main reason is that I did have some more time to research the topic, and the recommended way to do the play/pause button is to change only the label. If you want to read more on this, here are two good articles:
Display the Button Only for Users Who Prefer Reduced Motion
Alright, now we have a version with a toggle to play or pause the animations. Yay! However, there's one more thing to do, as we don't want to show the button to those who don't need the reduced motion and thus have the setting turned on. The button wouldn't do anything for them, and the GIF would be playing the animation all the time anyway. So let's hide it from these users.
We need the value of the user's preference on this media query. We could build this from scratch, but luckily Josh Comeau has written a blog post with a usePrefersReducedMotion
-hook, which we're going to use. I'll leave that code out of this blog post, but you can check it out from the link.
So, let's add the code:
const Gif = () => {
const [play, setPlay] = useState(false)
const handleToggle = () => setPlay(!setPlay)
const buttonText = play ? 'Pause' : 'Play'
const prefersReducedMotion = usePrefersReducedMotion()
return (
<div>
{prefersReducedMotion &&
<button onClick={handleToggle}>{buttonText}</button>
}
<picture>
...
</picture>
</div>
)
}
So, now we have a solution where those who prefer reduced motion can toggle the animation of a GIF, and those, who don't have any preferences, see the moving GIF all the time.
If you want to see the example in action, I've deployed a small example to my site. You can find the complete code from the repository:
eevajonnapanula / gifs-and-reduced-motion
An example of reduced motion and gifs.
The Demo
Here's the recording from React Finland's second vodcast, which had a theme of accessibility. Other guests in the episode are Nicolas Steenhout and Amy Carney. Unfortunately, there are no captions at the time of writing, but I was talking with the organizer, and they should be adding them as soon as they get the captions.
There were many interesting conversations in the episode, but if you're only after my demonstration, it starts from 1:26:10.
Top comments (1)
A great post on the proper way to handle GIFs and
prefers-reduced-motion
. ❤ 🦄 and followed!May I humbly submit my (hacky) solution for pausing GIFs without having to generate images server side etc. It would need a bit of work to put into production but the concept is simple, grab a frame from the GIF and then overlay the original GIF animation with the image, then make the original image transparent (well
opacity: 0.01
just in case that annoying ChromeVox opacity bug still exists withopacity: 0
!).Switch on
prefers-reduced-motion: reduce
and reload the page, all the GIFs will stop (or just press the button!)Beauty is because we can just hide the canvas with
aria-hidden="true"
androle="presentation"
it is accessible out of the box (assuming the original GIF has analt
attribute 😋). The same principle could make the GIFs play / pause individually with a little rework by simply removing the canvas and changing the image opacity to 1 again.I wrote about it here if the concept interests you!