Skip to main content

Animation Toggle v2.9.0

Warning: Individuals with vestibular motion disorders may wish to activate the "reduce motion" feature on their device prior to engaging with animations in this demo.

Allow users to pause or resume web animations, videos, or other dynamic content for improved control and accessibility.

<div class="animation-toggle">

  <video class="animation-toggle__video" loading="lazy">
    <source src="https://radancy.dev/component-library/animation-toggle/demo-lg.mp4">
  </video>

</div>
/*!

  Radancy Component Library: Animation Toggle

  Contributor(s):
  Michael "Spell" Spellacy

  Dependencies: Sass

*/

.animation-toggle { 
display: grid;
 
  // Control Container (Mandatory)
 
  &__controls {
  display: grid;
  gap: calc(12rem / 16);
  grid-auto-flow: column;
  justify-self: end;
  margin-block-start: calc(12rem / 16);
  margin-inline-end: calc(12rem / 16);
  z-index: 2;
  }
 
  // Toggle Buttons (Mandatory)
  // Important: Be sure to adjust colors as needed so that button stands out on darker or lighter backgrounds.
 
  &__pause, &__audio {
  background-color: #000;
  background-position: center;
  background-repeat: no-repeat;
  border-radius: calc(3em / 16);
  border: calc(1rem / 16) #fff solid;
  cursor: pointer;
  height: calc(30rem / 16);
  opacity: 0.5;
  outline-offset: 0.25em;
  transition: all 300ms ease-in-out;
  width: calc(30rem / 16);
    
    .animation-toggle:focus-within &, .animation-toggle:hover & {
    opacity: 1;
    }
 
  }
 
  &__pause {
  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAMAAAAp4XiDAAAAclBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9eWEHEAAAAJXRSTlMA8mB05+Tr4NqonE0f7si8sJWTdXNlXEk6MSTd0qKKf1UnFxEL+BREzAAAAKRJREFUSMftlscOg0AMBR/E9E5CTS/7/78YpA1klbUiwdlzfKM52wAedZjRjE/5DQZdQf5XHuMUE736JcJCZckESB1lUc3FVdkMqBVDqouXz7gIIZe4Okk4F8Dh5oNOXM45fLJfn+wkkUQSSSSRRJLt53XLEQ+5ufn7KsTcPOrkmTGuxJ1ZY3y4MLIHOmsssHC2ZIuJMQrImyE6NTBoczKkVw7AG83JGMcGlBNJAAAAAElFTkSuQmCC");
  background-size: 0.625rem;
 
    &[aria-pressed="true"] {
    background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOoAAAE2AQMAAABV0ZztAAAABlBMVEUAAAD///+l2Z/dAAAAAXRSTlMAQObYZgAAAipJREFUaN7t2rFNBjEMhuFDFJSM8I/CaHg0j+IRXLqw/CEQBc29KSyoSPtId7kUucT+rutZccF4qInfJAOW6OlPkuqenyV6+Ysk3fOrRHN7fLIzJ322VMxzy++frAMbcwDT1L9UxTzMOrAxB3MyN/Mw68DOHMzJ3MzDrAM7czAXczMPs4zZmYO5mJt5mGXMzpzMxdzMOrAxO3MyF/Mw68DGHMzJXMzDrAMbczAnczMPsw7szMGczM08zDqwMwdzMTfzMMuYnTmYi7mZdWBjduZkLuZm1oGN2ZmTuZiHWQc25mBO5mIeZh3YmIM5mZt5mHVgZw7mZG7mYZYxO3MwF3MzD7OM2ZmDuZibWQc2ZmdO5mJuZh3YmJ05mYt5mHVgYw7mYm7mYZYxO3N8881I5mLuFQ+zdmy/yf7Pf77m14aHuVdczMkczM5szKutp5iT2Zlts2E3czEHs2/+gsNczMkczMa8OrcUczA7syHP6qyYzMFszKsDeDEnszMb8qwuPckczMa8uioWczI7s23u381czMHsm6rFMBdzMjuzMa/qTMUczM68qs41czIHszGvCqbFHMzObMizKlInczAb86q0X8zJ7My26Zc0czIHs2+6TMNczMnszIY8q75gMQezM6+6qcWczMFszKsGd1FiQQpmZ76Qh7mZkzkoBiIZ84Xcd/z4uaR3nMyO4RkZ8nAyp5kTYz8KDA3JmDmR1JxnSuCH5MAvGtDrSXGPH9boP57gj/lMAAAAAElFTkSuQmCC")
    }
 
    &[disabled] {
    background-color: #333;
    background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAMAAAAp4XiDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDkuMC1jMDAwIDc5LjE3MWMyN2ZhYiwgMjAyMi8wOC8xNi0yMjozNTo0MSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI0LjAgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjFGNDQzOTFBMDNFNzExRUZBREE3RDY3NEY5MTNEMDlEIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjFGNDQzOTFCMDNFNzExRUZBREE3RDY3NEY5MTNEMDlEIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MUY0NDM5MTgwM0U3MTFFRkFEQTdENjc0RjkxM0QwOUQiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MUY0NDM5MTkwM0U3MTFFRkFEQTdENjc0RjkxM0QwOUQiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7n3NecAAAABlBMVEX///////9VfPVsAAAAAnRSTlP/AOW3MEoAAADVSURBVHjarNbbDsMgDAPQ5P9/epo2qQEnthetj4WzUciFSHjieJpxOr9VIQGgcMSJwgKHCVMUE654TPSCvMxmcN71SvjRXYMhBQwHXxagL1FBVad8iAgpMA3h5k2emd4eFHKteP6bg1AzLcYxEEBWFFxxOhpNpBmCwyOzISfupA6UHfWa/p5NUnyk2J35YLTRSTLHkhNTkDGmGfKFFhYkwtRETrPqtRUmvVS0q8RZLX8xWJPTLeOLZrFpSVP1Z41v0143TXxzVeDmj9eezeXKusK9BBgAWdwE4I4jwMQAAAAASUVORK5CYII=");
    cursor: default;
    }
 
    [data-pause-remove] & {
    display: none;
    }
 
  }
 
  &__audio {
  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAMAAAAp4XiDAAACQ1BMVEUAAACJiYn///+hoaH////09PRKSkq4uLicnJz9/f35+fn5+fnx8fHr6+vp6enl5eXGxsa4uLjMzMzHx8fV1dXHx8eIiIicnJx5eXmDg4OUlJSCgoL9/f329vb29vb29vbs7Ozx8fHr6+vv7+/h4eH////m5ubh4eHd3d3a2trQ0NDw8PDU1NTc3NzPz8/i4uLa2trDw8PPz8/j4+PY2NjT09Pj4+Pe3t7b29vPz8/Y2NjQ0NC4uLjd3d3j4+PV1dW7u7uLi4vNzc3FxcWrq6vMzMzHx8e3t7ednZ2ioqKKiorFxcWdnZ17e3u1tbWmpqalpaV4eHh2dnacnJy/v7+8vLysrKx4eHjQ0NCIiIi0tLRqamp+fn68vLy+vr41NTXt7e37+/v5+fn19fX4+Pjh4eH////z8/Pj4+P////////l5eXh4eHy8vLq6uru7u7e3t7U1NT19fXi4uLR0dHt7e3g4ODY2Njf39/u7u7q6urp6enOzs7w8PDb29vU1NTs7OzAwMDNzc3Gxsbn5+fKysrd3d3X19f///+vr6+UlJTz8/PS0tL///+pqam5ubnW1ta3t7fa2trCwsKqqqr///+kpKSgoKC3t7eXl5fY2NjDw8OkpKTp6em7u7uxsbGEhITR0dHo6OjBwcGYmJizs7OLi4u2trbIyMiKioqPj4+dnZ2+vr5lZWWnp6eYmJiHh4elpaWPj49QUFBPT089PT0xMTG9vb1qamo9PT0vLy+oqKj////9/f38/Pz29vby8vJp7g+xAAAAvHRSTlMAadiJkfETbgr8+u/r5tDKlJJzaV9bOSoVEA4G+PXp493b1tLR0M7Ix8S2tbWxr66sq6qmoqGbm5mXlZGMiX11cnJtbWlmZWJhWFhUUkpJRz8+NjUxJiMhHx8cHBgWDwX9+/Pz6efm4+Df3NjW09HOzczFw8G/v7++vLe0tLCrp6Oin5qUlJKQjo2KiYmIiIaFg4KCgoB+fXx8eXVzZ2NiYFxbWVdUU1JGRD88Ojg1Mi4tLSwnGBYUDgsJBDauXZEAAAKXSURBVEjHrZZnVxNBGEbHkoQivUlHQFAQkC6giFQpAgKCNLH33nvvvffee+++QX+aM89MsqtusmeP3E/32ZN7sslm94QNLaMjmi0njoKqHqtReBKlV8dYjEKJbCH91po3iURxN6w1PhuIaE2Hyau6a+/P0s1dxKn1nsSEL6WF+Y3Rrp0pmgvMhEoiii99p1aQaA6aNZ2BROQsicUY8BfNaWbGJuIkRsLbSNBg2uwkzs978GNoehl4NcLN8TOXqhqjtGYcXncLjlNLY+Aa/UVghY9KBubjwAzh0zVlw+gfbDWqmSm3XXiCsJWGCZiomlysjUIvQl94TGi9TOxyia/t46CwMn3yay75OrUmVzbZGFuFJgsL0CdF9s/27pY8d9OE5BncGc11PzRKl5xioJkUAZhf/TDquDpgT3TJSCa5S4rnmNvgwdzaYQ6DhK0lyWSsQ/AcbrNhU4ySqSTJxzoPH8+tf56wUqOkniQTsG7Cx3L7ZhNWbJQ8JkkG1jR38h1JkVFS+ccvoNx9Yl/w5ZUYJdkkOYx1RJ6k14//gRQPMbPgk7i9hYXqkqPqUbFI7TjMWBtGBdc7sAe65AQeWjWLSXEFSaQcXVyDYa91yah1aUHJ8eRiDAMhGKnue3RBLBIj/D/JxBernlsfdB/zlCzpksU57RfqgDZ5Svaou79PzjbhK3D22uXV48xoZYrVOHBZewyE4XDDsuFulqfkld3GE1z32QvgqfJNTChXV1F7JrWaFGfl1Ybb/XAHmRQHiLPqpRzpYmz2HkSliMtZDVcXPshr0L6dKKn4qWteFcUWr0Xv9cKwDph2Z+9lFjg5SDQYZiH4kUNEOzotFO8TiAIfWQh6dhMVtlgIosOzMiPmWPxzVYfg//kNYqtcGTkX0/4AAAAASUVORK5CYII=");
  background-size: calc(18rem / 16);
  }
 
  // Video (Optional)
 
  &__video {
  width: 100%;
  }
 
  // Backdrop (Optional)
 
  &__backdrop {
 
    &:not(.animation-enabled &) {
 
      &::after {
      background-color: rgba(0, 0, 0, 0.5);
      content: "";
      pointer-events: none;
      position: relative;
      z-index: 1;
      }
 
    }
 
  }
 
  // Primary Content (Optional)
 
  &__content {
  color: #fff;
  font-size: clamp(1rem, 9vw, 6rem);
  font-weight: bold;
  line-height: normal;
  place-self: center;
  text-align: center;
  z-index: 2;
  }
 
  // Audio Description Track (Optional)
 
  &__track {
  align-self: end;
  background-color: rgba(0,0,0,.8);
  border-radius: 0.3125em;
  color: #fff;
  display: none;
  font-size: 0.9rem;
  line-height: normal;
  margin: 1em;
  padding: 0.7em;
  text-align: center;
  z-index: 1;
 
    @media(min-width: 42.5em) {
 
      font-size: 1.1rem;
      justify-self: center;
 
    }
 
    &.active {
    display: block; 
 
      &:empty {
      display: none;
      }
 
    }
 
  }

  // Shared 

  &__controls, &__video, &__content, &__track, &__backdrop::after {
  grid-area: 1 / 1;
  }
 
 }
 
/*!

  Radancy Component Library: Animation Toggle

  Contributor(s):
  Michael "Spell" Spellacy

  Dependencies: None

*/

(() => {

  "use strict";

  const loadLanguagePack = (url, callback) => {

    // Install Language Pack.

    const getComponentLanguagePack = document.getElementById("component-library-language-pack");

    if (!getComponentLanguagePack) {

      const componentLanguagePack = document.createElement("script");

      componentLanguagePack.setAttribute("src", url);
      componentLanguagePack.setAttribute("id", "component-library-language-pack");
      componentLanguagePack.addEventListener("load", callback);

      document.head.appendChild(componentLanguagePack);

    } else {

      getComponentLanguagePack.addEventListener("load", callback);

    }

  };

  const initAnimationToggle = () => {

    loadLanguagePack("https://services.tmpwebeng.com/component-library/language-pack.js", () => {

      // Display which component in use via console:

      console.log("%cAnimation Toggle%cv2.9.0", "background: #2d2d2d; color: #fff; padding: 6px 10px; border-radius: 16px 0 0 16px; font-weight: 600;" , "background: #6e00ee; color: #fff; padding: 6px 10px; border-radius: 0 16px 16px 0; font-weight: 600;");

      // Animation variables

      const animationBody = document.body;
      const atAudioDescriptionClassName = "animation-toggle__audio";
      const atClass = ".animation-toggle";
      const atCallback = document.querySelectorAll(`${atClass}[data-callback]`);
      const atCookieName = "AnimationPaused";
      const atDescriptionTrackClass = ".animation-toggle__track";
      const atEnabledClassName = "animation-enabled";
      const atPauseButtonClassName = "animation-toggle__pause";
      const atPauseButtonClass = `.${atPauseButtonClassName}`;
      const atVideoClass = ".animation-toggle__video";
      const atVideoControlsName = "animation-toggle__controls";
      const dataAudioDescriptionButton = "data-audio-description-button";
      const dataAutoplayDisable = "data-autoplay-disable";
      const dataDecorative = "data-decorative";
      const dataLoop = "data-loop";
      const dataPauseButton = "data-pause-button";
      const dataPoster = "data-poster";
      const getAnimationWrappers = document.querySelectorAll(atClass);
      const getBackgroundVideos = document.querySelectorAll(atVideoClass);

      // Language

      const atAudioDescriptionLabel = window.atAudioDescriptionLabel;
      const atPauseButtonLabel = window.atPauseButtonLabel;
      const atVideoLabel = window.atVideoLabel;

      // Used to retrieve cookie and pause video(s) if present.

      const getCookie = name => {

        const match = document.cookie.match(RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
        return match ? match[1] : null;

      };

      // Assign cookie to variable on page load.

      let animationPaused = getCookie(atCookieName);

      // Used to set and remove cookie.

      const setCookie = state => {

        document.cookie = `${atCookieName}=${state}; SameSite=None; Secure; path=/`;

      };

      // If animation disabled in OS settings and cookie is not present then force pause.

      if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {

        if(animationPaused === null) {

          setCookie("true");

          animationPaused = getCookie(atCookieName);

        }

      }

      // For each animation wrapper...

      getAnimationWrappers.forEach(wrapper => {

        // Create control wrapper.

        const btnControls = document.createElement("div");

        btnControls.setAttribute("class", atVideoControlsName);

        // Prepend control wrapper to atClass.

        wrapper.prepend(btnControls);

        // Create pause button

        const btnPlayPause = document.createElement("button");

        // See if wrapper contains custom pause button value; use over default if true.

        btnPlayPause.setAttribute("aria-label", wrapper.hasAttribute(dataPauseButton) ? wrapper.getAttribute(dataPauseButton) : atPauseButtonLabel);

        // Add class to pause button.

        btnPlayPause.classList.add(atPauseButtonClassName);

        // Check to see if cookie is false or null.

        if(animationPaused === "false" || animationPaused === null) {

          btnPlayPause.setAttribute("aria-pressed", "false");

          animationBody.classList.add(atEnabledClassName);

        } else {

          btnPlayPause.setAttribute("aria-pressed", "true");

          animationBody.classList.remove(atEnabledClassName);

        }

        // Append pause button.

        btnControls.append(btnPlayPause);

        // Pause Toggle Event.

        btnPlayPause.addEventListener("click", function() {

          const getAtPauseButtonClass = document.querySelectorAll(atPauseButtonClass);
          const animationPauseToggles = getAtPauseButtonClass;

          if (this.getAttribute("aria-pressed") === "false") {

            animationBody.classList.remove(atEnabledClassName);

            setCookie("true");

            animationPauseToggles.forEach(button => {

              button.setAttribute("aria-pressed", "true");

            });

          } else {

            animationBody.classList.add(atEnabledClassName);

            setCookie("false");

            animationPauseToggles.forEach(button => {

              button.setAttribute("aria-pressed", "false");

            });

          }

          getBackgroundVideos.forEach(video => {

            if(!video.closest(atClass).querySelector(atPauseButtonClass).hasAttribute("disabled")) {

              const isPaused = video.closest(atClass).querySelector(atPauseButtonClass).getAttribute("aria-pressed");

              if(isPaused === "false") {

                video.play();

              } else {

                video.pause();

              }

            }

          });

          if(this.closest(atClass).hasAttribute("data-callback")) {

            const thisButton = this;
            const callBackFunction = this.closest(atClass).getAttribute("data-callback");

            customCallback(thisButton, callBackFunction);

          }

        });

        if (wrapper.querySelector("track") !== null) {

          const btnAudioDescription = document.createElement("button");

          btnAudioDescription.setAttribute("aria-label", wrapper.hasAttribute(dataAudioDescriptionButton) ? wrapper.getAttribute(dataAudioDescriptionButton) : atAudioDescriptionLabel);
          btnAudioDescription.setAttribute("aria-pressed", "false");
          btnAudioDescription.setAttribute("class", atAudioDescriptionClassName);

          btnControls.append(btnAudioDescription);

          btnAudioDescription.addEventListener("click", function () {

            const thisVideo = wrapper.querySelector(atVideoClass);
            const thisDescription = wrapper.querySelector(atDescriptionTrackClass);

            thisDescription.classList.toggle("active");

            const isPressed = this.getAttribute("aria-pressed") === "true";
            const track = thisVideo.textTracks ? thisVideo.textTracks[0] : null;

            if (!isPressed) {

              this.setAttribute("aria-pressed", "true");

              if (track) {

                track.mode = "hidden";

                track.oncuechange = function () {

                  const currentCue = this.activeCues[0];

                  if (currentCue) {

                    thisDescription.innerText = "";
                    thisDescription.appendChild(currentCue.getCueAsHTML());

                    const Message = thisDescription.textContent;
                    const msg = new SpeechSynthesisUtterance(Message);

                    window.speechSynthesis.speak(msg);

                  }

                };

              }

            } else {

              this.setAttribute("aria-pressed", "false");

              if (track) {

                track.oncuechange = null;

              }

              thisDescription.innerText = "";

              window.speechSynthesis.cancel();

            }

          });

        }

      });

      getBackgroundVideos.forEach((video, e) => {

        video.setAttribute("crossorigin", "anonymous")
        video.setAttribute("disableRemotePlayback", "");
        video.setAttribute("disablePictureInPicture", "");
        video.id = `animation-toggle-video-${e + 1}`;
        video.setAttribute("playsinline" , "");
        video.muted = true;

        video.load();

        if (animationBody.classList.contains(atEnabledClassName)) {

          if(!video.hasAttribute(dataAutoplayDisable)) {

            video.onloadeddata = () => {

              const playPromise = video.play();

              if (playPromise !== undefined) {

                playPromise.then(() => {

                  // Automatic playback started!

                }).catch(error => {

                  console.error('Playback error:', error);

                });

              }

            }

          }

        } else {

          if(video.hasAttribute(dataPoster)) {

            video.currentTime = video.getAttribute(dataPoster);

          }

        }

        if(video.hasAttribute(dataLoop)) {

          let iterations = 1;

          video.addEventListener("ended", () => {

            if (iterations < video.getAttribute(dataLoop)) {       

              video.currentTime = 0;
              video.play();
              iterations ++;

            } else {

              const toggleButton = video.closest(atClass).querySelector(atPauseButtonClass);

              if(toggleButton) {

                toggleButton.setAttribute("disabled", "");

              }

            }

          }, false);

        } else {

          video.setAttribute("loop" , "");

        }

        if(!video.hasAttribute("aria-label")) {

          video.setAttribute("aria-label", atVideoLabel);

        }

        if(video.hasAttribute(dataDecorative)) {

          video.setAttribute("aria-hidden", "true");
          video.setAttribute("tabindex", "-1");
          video.removeAttribute("aria-label");

        }

      });

      const customCallback = (thisButton, customCallBackName) => {

        if (customCallBackName !== null) {

          window[customCallBackName](thisButton);

        }

      };

      atCallback.forEach(callback => {

        const thisButton = callback.querySelector(atPauseButtonClass);
        const callBackFunction = callback.dataset.callback;

        customCallback(thisButton, callBackFunction);

      });

    });

  };

  initAnimationToggle();

})();

Responsible Animation Anchor (Responsible Animation)

Consider the impact that animation can have on people with disabilities. For some, animation can be a distraction; for others, it can cause discomfort or illness. In severe cases, it can pose serious danger, like triggering seizures in individuals with photosensitivity. By following these guidelines, we can ensure that our digital experiences are both enjoyable and safe for everyone!

  1. Any web animation that loops must include a pause button. There are no exceptions here. In applications where this cannot be controlled, such as email, animation should be avoided.
  2. Animations that occur once when interacting with a web page are exempt from having a pause button if they do not last for more than 5 seconds and are not excessive in their motion. However, developers should always exercise caution and honor user settings by including prefers-reduced-motion to help reduce or stop animation where needed. Please visit the Mozilla Developer Network for more details about this useful media query.
  3. Animations that flash more than 3 times per second are not allowed on any websites or applications, as they may induce seizures. A pause button will be ineffective here. If you suspect that an animation may be a risk, please bring this to the attention of the Accessibility Team.

Video Performance Anchor (Video Performance)

Be mindful of the negative impact background videos can have on page performance and consider these best practices when producing them:

Properties Anchor (Properties)

Use the following properties to configure this component.

ARIA Applies To Note Description
aria-label video String, Optional
Used to pass custom label to video element. This will override the default label, which is "Background Animation".
aria-live .animation-toggle__track Conditional
Required on .animation-toggle__track when present and must contain a value of "polite".

Attribute Applies To Note Description
data-audio-description-button .animation-toggle String, Optional
Used to pass custom label to .animation-toggle__audio. This will override the default label, which is "Audio Description".
data-autoplay-disable video Boolean, Optional
Used to prevent the video from autoplaying on page load. This may be useful when used with data-callback, where you may wish for the video to only load, for example, using an intersection observer.
data-callback .animation-toggle String, Optional
Used to define a custom callback function to initiate when animation first loads and pause button is accessed. Particularly useful for handling third-party scripts. The callback accepts a single parameter called thisButton that you can use to target the button itself, if needed.
data-decorative video Boolean, Optional
Used to define a purely decorative video, which will not be accessible to assistive technology or keyboard users. Often, this should be used with data-pause-remove.
data-loop video Number, Optional
Used to set the amount of times a video will loop. Please note that when a loop iteration is completed, a video cannot be restarted and the pause button will become disabled.
data-pause-button .animation-toggle String, Optional
Used to pass custom label to .animation-toggle__pause. This will override the default label, which is "Pause Animation".
data-pause-remove .animation-toggle Boolean, Optional
Used to remove a pause button from animation. This attribute should only be used in certain configurations. For example, short, non-looping animations.
data-poster video Number, Optional
Used to indicate at which point video will load when paused. Accepts any valid time increment. Video will start from this point when play is enabled.

Class Applies To Note Description
.animation-enabled body Component
Applied on page load. Page animations will only run when this class is present. Removing it pauses animation. If you are leveraging this class in your CSS or scripting, then there is no need to use prefers-reduced-motion.
.animation-toggle div, section Mandatory
Required on the parent element that wraps your animation and content.
.animation-toggle__audio button Component
Used on all audio description buttons.
.animation-toggle__backdrop .animation-toggle Optional
Append to .animation-toggle class. This will allow video to animate in on page load and for background shading to display when video is paused. Often used on hero banner and meant to improve contrast of foreground text.
.animation-toggle__controls div Component
Used to contain pause and audio description buttons.
.animation-toggle__pause button Component
Used on all pause buttons. This may never be hidden and is an integral part of the component.
.animation-toggle__track div Optional
Used on single div element that is a child of .animation-toggle. The aria-live attribute is required on this element when used.
.animation-toggle__video video Mandatory
Required on video element that is a child of .animation-toggle. Multiple videos may be used if desired.

Name Value Type Secure SameSite Domain Path
AnimationPaused true/false Persistent true None N/A /

Release Notes Anchor (Release Notes)

Keep up to date on major changes to this component.

Feature
April 07, 2026: Added support for loading=lazy to video for better performance support.
Note
April 24, 2025: Upgraded component to ES6 and fixed a small z-index bug in the Sass.

Behavior
Pausing one animation will result in a pause for all site-wide animations utilizing this script. Any animations that are paused will maintain their paused state when user returns to the site.
Behavior
The script utilizes prefers-reduced-motion. If a user disables system animations, all animations will honor this setting.
Behavior
When no cookie is set and reduce animation is enabled, the animation stops.
Behavior
When no cookie is set and reduce animation is not enabled, the animation plays.
Behavior
When cookie is set to false (not paused) and reduce animation is enabled, OS setting take precedent and cookie is set to true (paused).
Behavior
When reduce animation is not enabled, cookie settings take precedent.
Roadmap
When reduce animation is enabled, it would be great if local page settings could override OS setting.
Behavior
Phones in low power mode may automatically pause auto-playing videos to conserve battery life.
Testing
While the disableremoteplayback attribute may not be recognized as valid HTML, the error can be safely disregarded.

Behavior
When cookie is set to true (paused) and reduce animation is disabled, cookie is not set to false (not paused). Page needs to be refreshed. This may not be an issue, but to work, we may need another condition to hook into here.
Behavior
Revisit data-callback and see if it makes sense to apply callback function to all animations if data-callback present on page at all. Right now, there is an animation disconnect between those that have the attribute and those that do not. Currently, we manually add the callback to all other page animations.
Behavior
Include a reload button to enable playback of videos that loop only once, allowing users to replay them if desired.
Behavior
If data-pause-remove is used, override it temporarily to include play button, so that video does not obscure content. Especially true when transparent video is used.
Behavior
When phone is in low power mode the video is paused, but the toggle button does not reflect a paused state. See if video is playing or paused and alter state of toggle button.
Logic
Add a condition to check if the video is paused when using the data-poster attribute. This attribute does not work in low power mode, which can be problematic if the first frame has no content or images. While a quick fix is to use a poster element, we would prefer to avoid this solution.
Behavior
Add a possible replay button for videos that only play once.
Feature
Add a universal animation toggle button that can live anywhere on the page and be custom styled.

Report Issues

Find a bug? Want a new feature? Report it on JIRA. You're so awesome!