CSS keyframe animation for n number of elements

Viewed 111

I am looking to implement css keyframe animation for a set of logo images, logos must infinitely slide, here is how I am doing it:

.animated-logo-set {
  display: flex;
  flex-direction: column;
  height: 20px;
  overflow: hidden;
}
.animated-logo-set span {
  font-size: 20px;
  line-height: 20px;
  background-color: red;
  height: 20px;
  animation: slideup 5s infinite;
}
@keyframes slideup {
  0%,
  15% {
    transform: translateY(0);
  }
  50%,
  66% {
    transform: translateY(-20px);
  }
  90%,
  100% {
    transform: translateY(-40px);
  }
}
<div class="animated-logo-set">
  <span>Samsung</span>
  <span>Toyota</span>
  <span>Samsung</span>
</div>

This works fine if I know exactly how many logo images are to be animated. In my case, it can be any number of logo image from 2 to 20 for example, how do I deal with dynamic elements for such animation? is there a way we can do it using just the CSS or do we need to do it using JS? any pointer in here is appreciated.

Here is the link to CodeSandbox: https://codesandbox.io/s/blazing-forest-6oyk7w?file=/index.html:0-836.

Thank you.

2 Answers

It can only be done in CSS if you hardcode the values (e.g: you know how many items there are). To do it for any number of items, you need js.

Here's a vanilla js class for it:

class BannerAnimation {
  #index
  el
  items
  duration = 4000
  animationDuration = 420

  constructor(props) {
    const { start, ...args } = props
    Object.assign(this, args)
    this.#index = start || 1
    if (!this.items || !this.items.length) {
      this.items = [...this.el.children].map((c) => c.innerHTML)
    }
    ;[...this.el.children].forEach((c) => c.remove())
    this.appendChild(this.#index - 1)
    this.el.style.setProperty('--dur', `${this.animationDuration}ms`)
    Object.assign(this.el.style, {
      display: 'block',
      height: this.el.children[0].offsetHeight + 'px'
    })
    setTimeout(this.start, this.duration)
  }

  start = () => {
    const div = this.appendChild(this.#index)
    const first = this.el.children[0]
    first.style.marginTop = `-${first.offsetHeight}px`
    this.el.style.height = div.offsetHeight + 'px'
    this.el.classList.add('animating')
    setTimeout(this.end, this.animationDuration)
  }

  end = () => {
    this.el.classList.remove('animating')
    this.el.children[0].remove()
    this.#index = (this.items.length + this.#index + 1) % this.items.length
    setTimeout(this.start, this.duration)
  }

  appendChild = (i) => {
    const div = document.createElement('div')
    div.innerHTML = this.items[i]
    this.el.appendChild(div)
    return div
  }
}

new BannerAnimation({
  el: banner,
  duration: 1000,
  // items: [1, 2, 3]
})
#banner {
  overflow: hidden;
  transition: height 0ms cubic-bezier(.4, 0, .2, 1);
  transition-duration: var(--dur);
}
#banner div:first-child {
  margin-top: 0px
}
#banner div {
  transition: margin-top 0ms cubic-bezier(.4, 0, .2, 1);
  transition-duration: var(--dur);
}
<div id="banner">
  <div>One</div>
  <div>
    Two
    <div style="height: 100px; border: 1px solid red;"></div>
  </div>
  <div>Three</div>
  <div>Four</div>
  <div>Five<br>and a half...</div>
  <div>Six</div>
  <div>Seven</div>
</div>
<hr>

Should work with any number of items (at least 2). Can be used multiple times on the same page, for separate elements (but you'll need to update the CSS accordingly, of course). Children can vary in height.

Takes the following config, as object:

  • el: required (HTML element)
  • start: 1 based index (default: 1)
  • duration: ms between slide changes (default: 4000)
  • animationDuration: transition duration in ms (default: 420)
  • items: HTML strings array (default: this.el's children innerHTML) also works with numbers (e.g: items: [1, 2, 3]). In other words, in the example above, I could have left #banner empty and pass the items as ['One', 'Two'..., 'Seven'].

Here is how I attempted to solve my problem which works in my case.

<!DOCTYPE html>
<html lang="en">
  <head>
    <style>
      .animated-logo-set {
        display: flex;
        flex-direction: column;
        height: 20px;
        overflow: hidden;
        margin-bottom: 20px;
      }
      .animated-logo-set span {
        font-size: 20px;
        line-height: 20px;
        height: 20px;
      }
    </style>
    <script>
      document.addEventListener("DOMContentLoaded", function () {
        const blockHeight = 20;
        const logoAnimationDuration = 0.5;
        const logoStopDuration = 2;
        const logoItemDuration = logoAnimationDuration + logoStopDuration;
        const animatedLogoSets = document.querySelectorAll(
          ".animated-logo-set"
        );
        animatedLogoSets.forEach((element, index) => {
          const classNameWithIndex = "animated-logo-set-" + index;
          const keyframeName = classNameWithIndex + "-slideup";
          element.classList.add(classNameWithIndex);
          let logos = element.querySelectorAll("span");
          const firstElementClone = document.createElement("span");
          firstElementClone.innerHTML = logos[0].innerHTML;
          element.appendChild(firstElementClone);
          logos = element.querySelectorAll("span");
          const logoCount = logos.length;
          const logoSetDuration = logoCount * logoItemDuration;
          const logoStopPercentage = (logoStopDuration / logoSetDuration) * 100;
          const logoAnimationPercentage =
            (logoAnimationDuration / logoSetDuration) * 100;
          let css = "";
          css +=
            "." +
            classNameWithIndex +
            " span { animation: " +
            keyframeName +
            " " +
            logoSetDuration +
            "s infinite; } ";
          const frames = [];
          let lastPercentage = 0;
          let lastBlockHeight = 0;
          for (let i = 0; i < logoCount; i++) {
            frames.push({
              block_height: lastBlockHeight,
              min: parseInt(
                lastPercentage + (0 !== i ? logoAnimationPercentage : 0)
              ),
              max: parseInt(
                lastPercentage +
                  (0 === i
                    ? logoStopPercentage
                    : logoStopPercentage + logoAnimationPercentage)
              )
            });
            lastPercentage += logoStopPercentage + logoAnimationPercentage;
            lastBlockHeight -= blockHeight;
          }
          let keyframeCss = "@keyframes " + keyframeName + " { ";
          frames.forEach((frame) => {
            keyframeCss +=
              frame.min +
              "%, " +
              frame.max +
              "% { transform: translateY(" +
              frame.block_height +
              "px) } ";
          });
          keyframeCss += "}";
          const keyframeStyle = document.createElement("style");
          keyframeStyle.type = "text/css";
          keyframeStyle.innerHTML = keyframeCss;
          document.head.appendChild(keyframeStyle);

          const cssStyle = document.createElement("style");
          cssStyle.type = "text/css";
          cssStyle.innerHTML = css;
          document.head.appendChild(cssStyle);
        });
      });
    </script>
  </head>
  <body>
    <div class="animated-logo-container">
      <div class="animated-logo-set">
        <span>1</span>
        <span>2</span>
        <span>3</span>
        <span>4</span>
        <span>5</span>
        <span>6</span>
        <span>7</span>
        <span>8</span>
      </div>
      <div class="animated-logo-set">
        <span>1</span>
        <span>2</span>
        <span>3</span>
        <span>4</span>
      </div>
    </div>
  </body>
</html>

Code Sandbox Link

Related