Custom Scroll-Triggered Animation Effect
This snippet adds engaging animations to elements as they come into view while scrolling, enhancing user interaction.
Code
/**
* Custom Scroll-Triggered Animation Effect
* Adds an `.animate` class to `.animate-on-scroll` elements when they enter the viewport; lightweight, reusable, and performant.
* Usage: add `.animate-on-scroll` to elements; define CSS for `.animate` (and optional `.is-visible`) to trigger animations.
*/
(() => {
'use strict';
const SELECTOR = '.animate-on-scroll';
const ANIMATE_CLASS = 'animate';
const VISIBLE_CLASS = 'is-visible';
// Debounce limits how often we run expensive work during rapid events (scroll/resize).
const debounce = (fn, delay = 100) => {
let timeoutId;
return (...args) => {
window.clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => fn(...args), delay);
};
};
const safeQueryAll = (selector) => {
try {
return Array.from(document.querySelectorAll(selector));
} catch (err) {
console.error('[ScrollAnimate] Invalid selector:', selector, err);
return [];
}
};
const isInViewport = (el) => {
const { top, bottom } = el.getBoundingClientRect();
return top < window.innerHeight && bottom > 0;
};
const addAnimationClasses = (el) => {
el.classList.add(VISIBLE_CLASS, ANIMATE_CLASS);
};
const initScrollTriggeredAnimations = async ({
selector = SELECTOR,
debounceMs = 100,
once = true,
} = {}) => {
try {
let elements = safeQueryAll(selector);
if (!elements.length) return;
// Prefer IntersectionObserver for performance; fall back to scroll handler where unsupported.
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver(
(entries, obs) => {
entries.forEach(({ isIntersecting, target }) => {
if (!isIntersecting) return;
addAnimationClasses(target);
if (once) obs.unobserve(target);
});
},
{ root: null, threshold: 0.01 }
);
elements.forEach((el) => observer.observe(el));
return;
}
const handleScrollAnimation = () => {
elements.forEach((el) => {
if (el.classList.contains(ANIMATE_CLASS) && once) return;
if (isInViewport(el)) addAnimationClasses(el);
});
// If we're only animating once, we can stop checking elements already animated.
if (once) elements = elements.filter((el) => !el.classList.contains(ANIMATE_CLASS));
};
const onScroll = debounce(handleScrollAnimation, debounceMs);
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll, { passive: true });
handleScrollAnimation(); // Initial check on page load
} catch (err) {
console.error('[ScrollAnimate] Failed to initialize:', err);
}
};
// Initialize after DOM is ready; async-safe and avoids global scope pollution.
document.readyState === 'loading'
? document.addEventListener('DOMContentLoaded', () => void initScrollTriggeredAnimations())
: void initScrollTriggeredAnimations();
// Example usage:
// <div class="animate-on-scroll">...</div>
// CSS: .animate-on-scroll { opacity: 0; transform: translateY(16px); transition: 600ms ease; }
// .animate-on-scroll.animate { opacity: 1; transform: translateY(0); }
})();