DEV Community

Cover image for Hiding the Movement - GIFs, Play Buttons and prefers-reduced-motion
Eevis
Eevis

Posted on

Hiding the Movement - GIFs, Play Buttons and prefers-reduced-motion

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:

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>
)
Enter fullscreen mode Exit fullscreen mode

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.
  1. Open developer tools
  2. In the top-right corner, there is a menu with three dots, and is named "Customize and control Dev Tools." Open it.
  3. In the menu, there is a "More tools," and under it, "Rendering." Open it.
  4. Rendering opens to the bottom part of the developer tools. Scroll almost down the panel, and you'll find a section with different emulating options.
  5. Open the dropdown select from "Emulate CSS media feature prefers-reduced-motion." You can toggle between "prefers-reduced-motion: reduce" and "No emulation."
  6. It might take a second to update the value, but after that, you'll see what a person with the setting turned on would see.

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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:

GitHub logo 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.

Resources

Cover photo by he gong on Unsplash

Top comments (1)

Collapse
 
grahamthedev profile image
GrahamTheDev • Edited

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 with opacity: 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" and role="presentation" it is accessible out of the box (assuming the original GIF has an alt 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!