Activate CSS3 animation when the content scrolls into view
Asked Answered
B

7

61

I have a bar chart that animates with CSS3 and the animation currently activates as the page loads.

The problem I have is that the given bar chart is placed off screen due to lots of content before it so by the time a user scrolls down to it, the animation has already finished.

I was looking for ways either through CSS3 or jQuery to only activate the CSS3 animation on the bar chart when the viewer sees the chart.

<div>lots of content here, it fills the height of the screen and then some</div>
<div>animating bar chat here</div>

If you scroll down really fast right after page load, you can see it animating.

Here is a jsfiddle of my code. Also, I don't know if this matters, but I have several instances of this bar chart on the page.

I have come across a jQuery plug-in called waypoint but I had absolutely no luck getting it to work.

Braunite answered 1/5, 2013 at 20:11 Comment(0)
Z
72

Capture scroll events

This requires using JavaScript or jQuery to capture scroll events, checking each time a scroll event fires to see if the element is in view.

Once the element is in view, start the animation. In the code below, this is done by adding a "start" class to the element, that triggers the animation.

Updated demo

HTML

<div class="bar">
    <div class="level eighty">80%</div>
</div>

CSS

.eighty.start {
    width: 0px;
    background: #aae0aa;
    -webkit-animation: eighty 2s ease-out forwards;
       -moz-animation: eighty 2s ease-out forwards;
        -ms-animation: eighty 2s ease-out forwards;
         -o-animation: eighty 2s ease-out forwards;
            animation: eighty 2s ease-out forwards;
}

jQuery

function isElementInViewport(elem) {
    var $elem = $(elem);

    // Get the scroll position of the page.
    var scrollElem = ((navigator.userAgent.toLowerCase().indexOf('webkit') != -1) ? 'body' : 'html');
    var viewportTop = $(scrollElem).scrollTop();
    var viewportBottom = viewportTop + $(window).height();

    // Get the position of the element on the page.
    var elemTop = Math.round( $elem.offset().top );
    var elemBottom = elemTop + $elem.height();

    return ((elemTop < viewportBottom) && (elemBottom > viewportTop));
}

// Check if it's time to start the animation.
function checkAnimation() {
    var $elem = $('.bar .level');

    // If the animation has already been started
    if ($elem.hasClass('start')) return;

    if (isElementInViewport($elem)) {
        // Start the animation
        $elem.addClass('start');
    }
}

// Capture scroll events
$(window).scroll(function(){
    checkAnimation();
});
Zoarah answered 1/5, 2013 at 23:1 Comment(3)
Very good one, unfortunately it does not seem to work with Firefox when the element is in viewport and we refresh the page.Octad
@Octad for stuff like that I bind the event to multiple events: $(window).on('scroll scrollstart touchmove orientationchange resize') You could ofcourse add the 'load' event to that.Epicurus
The code distinction for var scrollElem = ((navigator.userAgent.toLowerCase().indexOf('webkit') != -1) ? 'body' : 'html'); is no longer valid for Chrome v70.Trogon
F
17

Sometimes you need the animation to always occur when the element is in the viewport. If this is your case, I slightly modified Matt jsfiddle code to reflect this.

jQuery

// Check if it's time to start the animation.
function checkAnimation() {
    var $elem = $('.bar .level');

    if (isElementInViewport($elem)) {
        // Start the animation
        $elem.addClass('start');
    } else {
        $elem.removeClass('start');
    }
}
Fleury answered 27/5, 2014 at 21:20 Comment(0)
S
15

You do not need to capture scroll events anymore

Since 2020, every browser is able to notify if an element is visible in your viewport.

With intersection observer.

I posted the code here: https://mcmap.net/q/324159/-css3-animate-elements-if-visible-in-viewport-page-scroll

Subtraction answered 23/6, 2020 at 18:49 Comment(0)
O
11

In order to activate a CSS animation, a class needs to be added to the element when this becomes visible. As other answers have indicated, JS is required for this and Waypoints is a JS script that can be used.

Waypoints is the easiest way to trigger a function when you scroll to an element.

Up to Waypoints version 2, this used to be a relatively simple jquery plugin. In version 3 and above (this guide version 3.1.1) several features have been introduced. In order to accomplish the above with this, the 'inview shortcut' of the script can be used:

  1. Download and add the script files from this link or from Github (version 3 is not yet available through CDNJS, although RawGit is always an option too).

  2. Add the script to your HTML as usual.

    <script src="/path/to/lib/jquery.waypoints.min.js"></script>
    <script src="/path/to/shortcuts/inview.min.js"></script>
    
  3. Add the following JS code, replacing #myelement with the appropriate HTML element jQuery selector:

    $(window).load(function () {
        var in_view = new Waypoint.Inview({
            element: $('#myelement')[0],
            enter: function() {
                $('#myelement').addClass('start');
            },
            exit: function() {  // optionally
                $('#myelement').removeClass('start');
            }
        });
    });
    

We use $(window).load() for reasons explained here.

Updated Matt's fiddle here.

Octad answered 28/4, 2015 at 11:24 Comment(1)
worked a treat for me! I used it with the animate.cssCaves
A
1

In addition to these answers please consider these points :

1- Checking the element in view has many considerations :
How to tell if a DOM element is visible in the current viewport?

2- If someone wanted to have more control over the animation (e.g. set "the animation type" and "start delay") here is a good article about it :
http://blog.webbb.be/trigger-css-animation-scroll/

3- And also it seems that calling addClass without a delay (using setTimeout) is not effective.

Alligator answered 19/7, 2016 at 4:55 Comment(0)
W
1

CSS FOR TRIGGER :

<style>
    .trigger{
      width: 100px;
      height: 2px;
      position: fixed;
      top: 20%;
      left: 0;
      background: red;
      opacity: 0;
      z-index: -1;
    }
</style>
<script>
        $('body').append('<div class="trigger js-trigger"></div>');

        $(document).scroll(function () {
 
           $('YOUR SECTIONS NAME').each(function () {

               let $this = $(this);

               if($this.offset().top <= $('.js-trigger').offset().top) {

                   if (!$this.hasClass('CLASS NAME FOR CHECK ACTIVE SECTION')) {
                       $this
                           .addClass('currSec')
                           .siblings()
                           .removeClass('currSec'); 
                   }
               }

           });

        });
</script>
Williemaewillies answered 6/12, 2017 at 9:20 Comment(0)
D
0

Following Adriano's answer (cannot mention or tag him here, maybe because it is my first ever post on StackOverflow), I managed to implement a pretty short solution for managing a set 3 separate animations for my landing page. Here's what the initial code looked like before my custom solution:

let options = {
  root: null,
  rootMargin: "0px",
  threshold: 0.3,
};

let observer = new IntersectionObserver(callbackFunc, options);

const ELEMENTS = document.querySelectorAll(/*Your elements's tags or classes.*/);

createObservers();

function createObservers() {
  Array.from(ELEMENTS).forEach((element) => {
    observer.observe(element);
  });
}

function callbackFunc(entries, observer) {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      // Your custom code.
    }
  });
}

Now, regarding my solution it is important to note that, for this to work, I put classes to my elements depending on the animation that I wanted to trigger on them. For example, most of the headings have a fade in effect from below. The class that I used for it was "below", like in this example:

<h1 class="below">My title.</h1>

And the animation along with the class attached to it were declared like this:

@keyframes fadeInFromBelow {
  0% {
    transform: translateY(8rem);
    opacity: 0;
  }
  100% {
    transform: translateY(0);
    opacity: 1;
  }
}
.fade-in-from-below {
  animation: 1s ease-out 0s 1 fadeInFromBelow;
  opacity: 1;
}

Finally, here's what the code looks like with my custom code/solution in it (with some comments to help you understand what's happening). I hope this is useful for someone!:

`
// Declare options to be passed to the observer.
let options = {
  root: null,
  rootMargin: "0px",
  threshold: 0.3,
};

// Declare observer.
let observer = new IntersectionObserver(callbackFunc, options);

// Declare all elements that might be animated, by tagname or classname, in a single variable.
const ELEMENTS = document.querySelectorAll(
  "h1,h2,h3,p,details,span,li,input,select,textarea,nav,.header-logo,.ham"
);

// Execute function to generate an observer for each element.
createObservers();

function createObservers() {
  // Convert the 'ELEMENTS' variable to an array, there might be another workaround for this, so it might be not necessary but this worked just fine for me.
  const list = Array.from(ELEMENTS);
  list.forEach((element) => {
    observer.observe(element);
  });
}

// This is just optional, these are just variables for the classes that are going to be added to the elements in order to trigger whichever animation you defined in them.
const LEFT = "slide-in-from-left";
const RIGHT = "slide-in-from-right";
const BELOW = "fade-in-from-below";

// Finally, the function were your custom code/solution will be.
function callbackFunc(entries, observer) {
  entries.forEach((entry) => {
    // This declaration is optional, it allowed me to make the code more readable and less cluttered.
    const e = entry.target.classList;
    // First, we check if the element is intersecting the viewport, based on the 'treshhold' attribute that we declared in the 'options' from above.
    if (entry.isIntersecting) {
      e.add(e.contains("left") && !e.contains(LEFT) && LEFT);
      e.add(e.contains("right") && !e.contains(RIGHT) && RIGHT);
      e.add(e.contains("below") && !e.contains(BELOW) && BELOW);
      e.remove("no-opacity");
    }
  });
}

`

Dambro answered 21/2 at 17:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.