Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trigger a function when the user scrolls the element into the viewport – Vanilla JavaScript

I have written a function which animates a bar chart. Currently this action is being triggered on page load, but I only want it triggered when I get to the element – otherwise the user wont see the animation. I would like to achieve this through vanilla JavaScript, is it possible?

Here is my mark up:

<div class="section">
  section
</div>
<div class="section">
  section
</div>
<div class="section">
  section
</div>
<div class="section">
  section
  <ul class="skills__list">
    <li class="skills__list-item">
      <div class="bar">
        <span>HTML</span>
        <div class="bar__inner" data-percent="90%"></div>
      </div>
    </li>
    <li class="skills__list-item">
      <div class="bar">
        <span>css</span>
        <div class="bar__inner" data-percent="80%"></div>
      </div>
    </li>
    <li class="skills__list-item">
      <div class="bar">
        <span>Javascript</span>
        <div class="bar__inner" data-percent="60%"></div>
      </div>
    </li>
    <li class="skills__list-item">
      <div class="bar">
        <span>UI design</span>
        <div class="bar__inner" data-percent="70%"></div>
      </div>
    </li>
    <li class="skills__list-item">
      <div class="bar">
        <span>sketch</span>
        <div class="bar__inner" data-percent="50%"></div>
      </div>
    </li>
    <li class="skills__list-item">
      <div class="bar">
        <span>Photoshop</span>
        <div class="bar__inner" data-percent="80%"></div>
      </div>
    </li>
    <li class="skills__list-item">
      <div class="bar">
        <span>Illustrator</span>
        <div class="bar__inner" data-percent="90%"></div>
      </div>
    </li>
  </ul>
</div>

And here is the Scss:

*{
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}

.section {
  padding: 20px;
  font-size: 30px;
  font-family: sans-serif;
  text-transform: uppercase;
  height: 400px;
  &:nth-child(1){
    background-color: #ddd;
  }
  &:nth-child(2){
    background-color: #aaa;
  }
  &:nth-child(3){
    background-color: #bbb;
  }
  &:nth-child(4){
    background-color: #000;
  }
}

.skills__list-item {
  & + .skills__list-item {
    margin-top: 20px;
  }
}

// bar chart styles
.bar {
  position: relative;
  width: 100%;
  height: 28px;
  overflow: hidden;
  background-color: blue;

  span {
    position: absolute;
    z-index: 9;
    display: flex;
    align-items: center;
    height: 100%;
    padding: 10px;
    color: #fff;
    background-color: red;
  }

  &__inner {
    display: flex;
    justify-content: flex-end;
    width: 0;
    height: 100%;
    background-color: green;
    transform-origin: 0 100%;
    transition: width 0.6s linear;

    &::after {
      content: attr(data-percent);
      align-self: center;
      padding-right: 20px;
      color: #fff;
    }
  }
}

And here is the JavaScript:

const bars = document.querySelectorAll('.bar__inner');


Array.from(bars).forEach((bar, index) => {
  setTimeout(() => {
    const eachBar = bar;
    eachBar.style.width = bar.dataset.percent;
  }, index * 400);
});

Here's a working example Codepen.

like image 554
Antony Moss Avatar asked Jan 02 '23 07:01

Antony Moss


2 Answers

You can follow a very helpful tip of the website Gomakethings.com.

It shows that you could use the getBoundingClientRect() method to achieve your goal:

// Get the an HTML element
var element = document.querySelector('<a selector>');

// Get its bounding client rectangle
var bounding = element.getBoundingClientRect();

Use it to build a function which checks if the element is in the viewport client by retrieving the bounding box (okay, the code could be improved, it's just a demo):

function isInViewport(element) {
    // Get the bounding client rectangle position in the viewport
    var bounding = element.getBoundingClientRect();
    
    // Checking part. Here the code checks if it's *fully* visible
    // Edit this part if you just want a partial visibility
    if (
        bounding.top >= 0 &&
        bounding.left >= 0 &&
        bounding.right <= (window.innerWidth || document.documentElement.clientWidth) &&
        bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight)
    ) {
        console.log('In the viewport! :)');
        return true;
    } else {
        console.log('Not in the viewport. :(');
        return false;
    }
}

Finally, add an event listener on scroll event which call the above function:

window.addEventListener('scroll', function (event) {
    if (isInViewport(theElementToWatch)) {
      // update the element display
    }
}, false);

According to the browser compatibility displayed by the MDN page, getBoundingClientRect() is fully supported by Chrome and Firefox (>= 3.5). The provided solution here is fully supported by the most used browsers (it's unknown for some mobile versions).

According to the one provided by Can I use... website, mobile browsers (Chrome, Firefox, etc.) fully support the method, at least from a given version.

Finally, you could keep in mind a still-experimental solution that aims to replace the use of the method getBoundingClientRect(), the Intersection Observer API (see Tom M's answer). The MDN related page explains why:

Implementing intersection detection in the past involved event handlers and loops calling methods like Element.getBoundingClientRect() to build up the needed information for every element affected. Since all this code runs on the main thread, even one of these can cause performance problems. When a site is loaded with these tests, things can get downright ugly.

like image 153
Amessihel Avatar answered Feb 12 '23 15:02

Amessihel


You can use the IntersectionObserver API for this.

const observer = new IntersectionObserver((entries) => { 
    entries.forEach((entry) => {
        console.log(entry.target);
        // your callback here
    })
}); 

observer.observe(document.querySelector('.skills__list-item'));

Mind the Browser support though, it's not supported in IE, Safari and Opera Mini for instance.

enter image description here

If you want to support IE7+ you can use the polyfill provided by the W3C though.

Resources

  • MDN Documentation
  • A cool blog post by david walsh
like image 45
Tom M Avatar answered Feb 12 '23 14:02

Tom M