Home > Mobile >  CSS keyframe animation for n number of elements
CSS keyframe animation for n number of elements

Time:08-08

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

CodePudding user response:

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>
  <div>Three</div>
  <div>Four<br> four and a half...</div>
  <div>Five</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).

It could be improved to allow items of different heights and neatly animate the height of the container, but I didn't bother with that, it seemed out of scope.

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'].

CodePudding user response:

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 >
      <div >
        <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 >
        <span>1</span>
        <span>2</span>
        <span>3</span>
        <span>4</span>
      </div>
    </div>
  </body>
</html>

Code Sandbox Link

  • Related