prefers-reduced-motion

How to manage animations on the website and make them accessible.

Time to read: 7 min

Briefly

One of the uses of the @media directive to check user settings for animation playback.

Most modern operating systems allow users to adjust animation settings. The media query prefers-reduced-motion allows you to determine whether animation is disabled or reduced in the system and apply CSS styles that take this into account.

With prefers-reduced-motion, you can slow down or completely disable animations.

How to Write It

The prefers-reduced-motion has two values:

  • no-preference — default animation settings.
  • reduce — animation is disabled.

In the example, we add smooth scrolling only for users who have not disabled animations at the system level.

        
          
          @media (prefers-reduced-motion: no-preference) {  html {    scroll-behavior: smooth;  }}
          @media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

        
        
          
        
      

Why?

A user may disable animation for various reasons, for example:

  • not everyone perceives animation the same way. What may seem smooth and pleasant to some can irritate or distract others (flashing ads, complex parallax effects, autoplaying videos);
  • medical aspects: some people may have vestibular disorders that can cause dizziness, nausea, or seizures even with simple animations;
  • websites with a lot of animation can quickly drain the battery of mobile devices or use more traffic. For example, autoplaying videos will require more data than displaying a static image.

Examples of Use

Disabling or Slowing Down Animation

To disable an element's animation or change its speed if the user has explicitly set a preference to reduce it, you can write the following in CSS:

        
          
          .button {  /*  Fun shaking button animation */  animation: shake 300ms linear infinite both;}/*  Completely disables animation */@media (prefers-reduced-motion: reduce) {  .button {    animation: none;  }}/* Slows down the animation by 2 times */@media (prefers-reduced-motion: reduce) {  .button {    animation: shake 600ms linear infinite both;  }}
          .button {
  /*  Fun shaking button animation */
  animation: shake 300ms linear infinite both;
}

/*  Completely disables animation */
@media (prefers-reduced-motion: reduce) {
  .button {
    animation: none;
  }
}

/* Slows down the animation by 2 times */
@media (prefers-reduced-motion: reduce) {
  .button {
    animation: shake 600ms linear infinite both;
  }
}

        
        
          
        
      

Conversely, we can play the animation only if the user has no preferences for showing animation:

        
          
          @media (prefers-reduced-motion: no-preference) {  .button {    animation: shake 300ms linear infinite both;  }}
          @media (prefers-reduced-motion: no-preference) {
  .button {
    animation: shake 300ms linear infinite both;
  }
}

        
        
          
        
      

The second method has two advantages:

  • less code;
  • older browsers that do not support prefers-reduced-motion will simply ignore this rule and display only the original element without animation.

Smooth Scrolling

        
          
          html {  scroll-behavior: smooth;}
          html {
  scroll-behavior: smooth;
}

        
        
          
        
      

If you set scroll-behavior: smooth on <html>, when the user clicks on an anchor link, the page will smoothly scroll to the desired position.

Unfortunately, there is currently no control over the page scroll speed in CSS. If the page is long, the scrolling might be very fast, which can be unpleasant for people sensitive to sudden animations.

You can wrap scroll-behavior in a media query to prevent smooth scrolling and simply open the page in the desired place if the user has changed animation settings:

        
          
          @media (prefers-reduced-motion: no-preference) {  html {    scroll-behavior: smooth;  }}
          @media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

        
        
          
        
      

What About JavaScript?

If you need to know animation preferences using JavaScript, you can do so with matchMedia. Here’s how this scrolling behavior setting looks in JavaScript:

        
          
          const prefersReducedMotion = window.matchMedia(  '(prefers-reduced-motion: reduce)')a.addEventListener('click', () => {  const behavior = prefersReducedMotion.matches ? 'auto' : 'smooth'  window.scrollTo({    x: 0,    y: 0,    behavior  })})
          const prefersReducedMotion = window.matchMedia(
  '(prefers-reduced-motion: reduce)')

a.addEventListener('click', () => {
  const behavior = prefersReducedMotion.matches ? 'auto' : 'smooth'

  window.scrollTo({
    x: 0,
    y: 0,
    behavior
  })
})

        
        
          
        
      

Optimizing Style and Library Loading

If you have a lot of CSS related to animations, you can separate the playback styles into a different file and not load it for users who have opted out of animations:

        
          
          <link  rel="stylesheet"  href="animations.css"  media="(prefers-reduced-motion: no-preference)">
          <link
  rel="stylesheet"
  href="animations.css"
  media="(prefers-reduced-motion: no-preference)"
>

        
        
          
        
      

Similarly, you can prevent loading heavy animation libraries. In the example below, if the user prefers to reduce the amount of animation, the function will make a return and its execution will be halted. As a result, GreenSock (GSAP) will not be imported.

        
          
          const prefersReducedMotion = window.matchMedia(  '(prefers-reduced-motion: reduce)')const loadGSAPAndInitAnimations = () => {  if (prefersReducedMotion.matches) return  import('gsap').then((object) => {    const gsap = object.default    // Initialize the animation using GSAP here  })};loadGSAPAndInitAnimations()
          const prefersReducedMotion = window.matchMedia(
  '(prefers-reduced-motion: reduce)')

const loadGSAPAndInitAnimations = () => {
  if (prefersReducedMotion.matches) return

  import('gsap').then((object) => {
    const gsap = object.default
    // Initialize the animation using GSAP here
  })
};

loadGSAPAndInitAnimations()