Search code examples
csscss-animationskeyframe

CSS keyframe animation for n number of elements


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.


Solution

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