0

Problem

I created a counter using HTML, CSS and JS (such as satisfied customer numbers, branch numbers, etc.) The counter is also animated but since it's down the page, I'd like to animate it only when it gets to that point on the page. How do I do with the js?

const counters = document.querySelectorAll('.value');
const speed = 400;

counters.forEach( counter => {
   const animate = () => {
      const value = +counter.getAttribute('akhi');
      const data = +counter.innerText;
     
      const time = value / speed;
     if(data < value) {
          counter.innerText = Math.ceil(data + time);
          setTimeout(animate, 1);
        }else{
          counter.innerText = value;
        }
     
   }
   
   animate();
});
.counter-box {

    display: block;
    background: #f6f6f6;
    padding: 40px 20px 37px;
    text-align: center

}
.counter-box p {

    margin: 5px 0 0;
    padding: 0;
    color: #909090;
    font-size: 18px;
    font-weight: 500

}

.counter { 

    display: block;
    font-size: 32px;
    font-weight: 700;
    color: #666;
    line-height: 28px

}
.counter-box.colored {

      background: #eab736;

}
.counter-box.colored p,
.counter-box.colored .counter {

    color: #fff;

}
        <div class="container">
          <div class="row contatore">
            <div class="col-md-4">
              <div class="counter-box colored">
                <span class="counter value" akhi="560">0</span>
                   <p>Countries visited</p>
              </div>
            </div>

    <div class="col-md-4">
       <div class="counter-box">
           <span class="counter value" akhi="3275">0</span>
              <p>Registered travellers</p>
       </div>
    </div>

    <div class="col-md-4">
        <div class="counter-box">
            <span class="counter value" id="conta" akhi="289">0</span>
               <p>Partners</p>
        </div>
    </div>
         </div> 
        </div>

What I have tried

i had tried with

const target = document.querySelector('.counter');
observer.observe(target);

but it doesn't seem to work. Many thanks to whoever can help me.

Anonymous
  • 835
  • 1
  • 5
  • 21
  • Use [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) for such purpose – Mad7Dragon Dec 03 '22 at 11:31
  • `ahki` is an invalid HTML5 attribute. Use `data-*` attributes instead. https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes – Roko C. Buljan Dec 03 '22 at 14:05
  • Possible **duplicate** of: [Restart counter animation when element in viewport](https://stackoverflow.com/questions/73369844/how-to-restart-counter-animation-when-its-out-of-view) – Roko C. Buljan Dec 03 '22 at 14:10

2 Answers2

3

I would recommend, as others have suggested, to use the Intersection Observer API to animate your elements once they appear in the viewport.

The idea is simple, we'll create an observer that will observe the counters to animate and we're going to configure it so that it calls the animate function once a counter is fully visible in the viewport.

You may learn more about the options that an IntersectionObserver can accept in order to customize its behavior. Meanwhile, here's a live demo that illustrates how to make the counters animate once they appear in the screen (the code below has some helpful comments):

const counters = document.querySelectorAll('.value'),
  speed = 400,
  /**
   * create an IntersectionObserver with the specified callback that will be executed for each intersection change for every counter we have. 
   * You may customize the options (2nd argument) per you requirement
   */
  observer = new IntersectionObserver(
    entries => entries.forEach(entry => entry.isIntersecting && animate(entry.target)), 
    {
      threshold: 1 // tells the browser that we only need to execute the callback only when an element (counter) is fully visible in the viewport
    }
  ),
  // the animate function now accepts a counter (HTML element)
  animate = counter => {
    const value = +counter.dataset.akhi,
      data = +counter.innerText,
      time = value / speed;
    if (data < value) {
      counter.innerText = Math.ceil(data + time);
      setTimeout(() => animate(counter), 1);
    } else {
      counter.innerText = value;
    }
  };

// attach the counters to the observer
counters.forEach(c => observer.observe(c));
.counter-box {
  display: block;
  background: #f6f6f6;
  padding: 40px 20px 37px;
  text-align: center
}

.counter-box p {
  margin: 5px 0 0;
  padding: 0;
  color: #909090;
  font-size: 18px;
  font-weight: 500
}

.counter {
  display: block;
  font-size: 32px;
  font-weight: 700;
  color: #666;
  line-height: 28px
}

.counter-box.colored {
  background: #eab736;
}

.counter-box.colored p,
.counter-box.colored .counter {
  color: #fff;
}
<div class="container">
  <div class="row contatore">
    <div class="col-md-4">
      <div class="counter-box colored">
        <!-- it is recommended to use "data-*" attributes to cache data that we might use later. The "data-akhi" contains the number to animate -->
        <span class="counter value" data-akhi="560">0</span>
        <p>Countries visited</p>
      </div>
    </div>
    <div class="col-md-4">
      <div class="counter-box">
        <span class="counter value" data-akhi="3275">0</span>
        <p>Registered travellers</p>
      </div>
    </div>
    <div class="col-md-4">
      <div class="counter-box">
        <span class="counter value" id="conta" data-akhi="289">0</span>
        <p>Partners</p>
      </div>
    </div>
  </div>
</div>
ThS
  • 4,597
  • 2
  • 15
  • 27
  • You should rather use `requestAnimationFrame` instead of `setTimeout(fn, 1)`. For an example see: [Restart counter animation when element in viewport](https://stackoverflow.com/a/73381458/383904) – Roko C. Buljan Dec 03 '22 at 14:14
  • 1
    @RokoC.Buljan I hugely recommend your suggestion due to its performance impact but if i was to change that as well in my answer, it'll get out of the context of the question. Nevertheless, crucial suggestion. – ThS Dec 03 '22 at 14:32
0

As others suggested, you should use Intersection Observer. This is how I'd do: Scrolldown the snippet in order to see the counter animating up once is on the screen.

const counters = document.querySelectorAll('.value');
const speed = 400;

const observer = new IntersectionObserver( items => {
    
  if(items[0].isIntersecting) { 
    const target = items[0].target;
    const animate = () => {
      const value = + target.getAttribute('akhi');
      const data = + target.innerText;
     
      const time = value / speed;
      if(data < value) {
          target.innerText = Math.ceil(data + time);
          setTimeout(animate, 1);
      }else{
          target.innerText = value;
      }     
   }   
   animate(); 
   observer.unobserve(target);
  }
})

counters.forEach( counter => observer.observe(counter));
.counter-box {

    display: block;
    background: #f6f6f6;
    padding: 40px 20px 37px;
    text-align: center

}
.counter-box p {

    margin: 5px 0 0;
    padding: 0;
    color: #909090;
    font-size: 18px;
    font-weight: 500

}

.counter { 

    display: block;
    font-size: 32px;
    font-weight: 700;
    color: #666;
    line-height: 28px

}
.counter-box.colored {

      background: #eab736;

}
.counter-box.colored p,
.counter-box.colored .counter {

    color: #fff;

}
<div style="height: 600px;">

</div>


<div class="container">
          <div class="row contatore">
            <div class="col-md-4">
              <div class="counter-box colored">
                <span class="counter value" akhi="560">0</span>
                   <p>Countries visited</p>
              </div>
            </div>

    <div class="col-md-4">
       <div class="counter-box">
           <span class="counter value" akhi="3275">0</span>
              <p>Registered travellers</p>
       </div>
    </div>

    <div class="col-md-4">
        <div class="counter-box">
            <span class="counter value" id="conta" akhi="289">0</span>
               <p>Partners</p>
        </div>
    </div>
         </div> 
        </div>
gugateider
  • 1,983
  • 2
  • 13
  • 17
  • You should use valid HTML5 `data-*` attributes, not invalid ones like `akhi` – Roko C. Buljan Dec 03 '22 at 14:12
  • @RokoC.Buljan There is always the theory police on Stackoverflow. I understand what you're referring to but it's entirely out of the scope of the question. – gugateider Dec 03 '22 at 14:23
  • It is out of scope, but never hurst to add a *"PS"* to your answer and improve further. At least by a mention. We wrote more words in comments than you would by just saying: *"Remember, avoid the use of custom attributes. Use `data-*`(MDN link) instead."* – Roko C. Buljan Dec 03 '22 at 14:26