# How to Implement Infinite Scroll and Animation on Scroll Events in JavaScript

> Learn to implement infinite scroll and animation on scroll events with JavaScript. Discover efficient pagination detection and viewport-triggered animations using IntersectionObserver and getBoundingClientRect.

- Repository: [Jisan Mia/dom-projects](https://github.com/jisan-mia/dom-projects)
- Tags: tutorial
- Published: 2026-03-04

---

**To implement infinite scroll and animation on scroll events, use IntersectionObserver for efficient pagination detection and getBoundingClientRect() with CSS class toggling for viewport-triggered animations, as demonstrated in the jisan-mia/dom-projects repository.**

The jisan-mia/dom-projects repository provides production-ready examples of performant front-end patterns. Learning how to implement infinite scroll and animation on scroll events enables you to build dynamic, fluid interfaces that automatically load content and animate elements as they enter the viewport using vanilla JavaScript.

## Animation on Scroll: The Foundational Pattern

The repository's [`projects/animation-on-scroll/script.js`](https://github.com/jisan-mia/dom-projects/blob/main/projects/animation-on-scroll/script.js) demonstrates a lightweight technique for detecting when elements enter the viewport. This implementation uses a scroll event listener combined with coordinate calculations to trigger CSS transitions.

### Viewport Detection Logic

The `checkAndShowBoxes()` function in [`projects/animation-on-scroll/script.js`](https://github.com/jisan-mia/dom-projects/blob/main/projects/animation-on-scroll/script.js) calculates a trigger line at 80% of the viewport height using `window.innerHeight / 5 * 4`. It then compares each element's position against this threshold using `getBoundingClientRect().top`:

```javascript
// projects/animation-on-scroll/script.js
const boxes = document.querySelectorAll('.box');

window.addEventListener('scroll', checkAndShowBoxes);
checkAndShowBoxes();

function checkAndShowBoxes() {
  const screenBottom = window.innerHeight / 5 * 4;
  boxes.forEach((box) => {
    const boxTopPosition = box.getBoundingClientRect().top;
    if (boxTopPosition < screenBottom) {
      box.classList.add('show');
    } else {
      box.classList.remove('show');
    }
  });
}

```

This approach fires on every scroll pixel, allowing immediate visual feedback. The browser handles the actual animation on the compositor thread, ensuring smooth performance.

### CSS Transition Architecture

The accompanying [`projects/animation-on-scroll/style.css`](https://github.com/jisan-mia/dom-projects/blob/main/projects/animation-on-scroll/style.css) file positions `.box` elements off-screen initially using `transform: translateX(±400%)`. When the JavaScript adds the `.show` class, the transform resets to `translateX(0)` with a smooth transition. This hardware-accelerated technique avoids layout thrashing by modifying only transform and opacity properties.

## Implementing Infinite Scroll with IntersectionObserver

For infinite scroll functionality, **IntersectionObserver** provides superior performance compared to scroll event listeners. While scroll events fire continuously, IntersectionObserver executes callbacks only when a target element crosses a viewport threshold.

### The Sentinel Element Pattern

Place an empty sentinel element at the bottom of your content feed. When this sentinel becomes visible, it triggers the next data batch. This pattern is referenced in [`projects/question-list-progress/data.js`](https://github.com/jisan-mia/dom-projects/blob/main/projects/question-list-progress/data.js), which includes the infinite scroll as a recognized implementation.

```javascript
// Infinite scroll implementation pattern
const feed = document.getElementById('feed');
const sentinel = document.getElementById('sentinel');
let page = 0;
const PAGE_SIZE = 20;

function generateItems(page) {
  const items = [];
  for (let i = 1; i <= PAGE_SIZE; i++) {
    const id = page * PAGE_SIZE + i;
    items.push({ title: `Item #${id}`, body: `Content for item ${id}` });
  }
  return items;
}

function renderItems(items) {
  const fragment = document.createDocumentFragment();
  items.forEach(({ title, body }) => {
    const card = document.createElement('div');
    card.className = 'card';
    card.innerHTML = `<h3>${title}</h3><p>${body}</p>`;
    fragment.appendChild(card);
  });
  feed.appendChild(fragment);
}

function onIntersection(entries) {
  if (entries[0].isIntersecting) {
    const newItems = generateItems(++page);
    renderItems(newItems);
  }
}

const observer = new IntersectionObserver(onIntersection, {
  root: null,
  rootMargin: '0px',
  threshold: 0.1
});
observer.observe(sentinel);

```

**Performance advantages** of this approach include:

- **Reduced CPU usage**: Callbacks execute only when visibility changes, not on every scroll pixel
- **Eliminated layout thrashing**: The browser calculates visibility natively without `getBoundingClientRect()` calls
- **Cleaner code**: No manual scroll offset tracking or debounce logic required

## Combining Both Patterns for Dynamic Content

To implement infinite scroll and animation on scroll events simultaneously, invoke the existing animation check immediately after rendering new content. This ensures newly loaded items animate into view without waiting for the next user scroll.

After appending items in your IntersectionObserver callback, call `checkAndShowBoxes()` to evaluate the new elements against the viewport threshold:

```javascript
function onIntersection(entries) {
  if (entries[0].isIntersecting) {
    const newItems = generateItems(++page);
    renderItems(newItems);
    // Trigger animation for newly added elements
    checkAndShowBoxes();
  }
}

```

Alternatively, allow the existing scroll listener to detect new elements naturally. Since `checkAndShowBoxes()` queries the DOM using `document.querySelectorAll('.box')` on every scroll event, it automatically includes dynamically appended elements.

### Critical Implementation Details

When building the combined solution:

1. **Structure**: Use a container for feed items, followed immediately by the sentinel element with minimal height (e.g., `height: 1px`)
2. **CSS**: Define initial states with `transform: translateY(50px)` and `opacity: 0`, transitioning to `transform: translateY(0)` and `opacity: 1` when the `.show` class is applied
3. **Timing**: Ensure the IntersectionObserver threshold (e.g., `0.1`) provides adequate buffer to load content before the user reaches the absolute bottom

## Summary

- **Scroll animations** in [`projects/animation-on-scroll/script.js`](https://github.com/jisan-mia/dom-projects/blob/main/projects/animation-on-scroll/script.js) use `getBoundingClientRect().top` compared against `window.innerHeight / 5 * 4` to toggle CSS classes when elements enter the viewport.
- **Infinite scroll** relies on IntersectionObserver watching a sentinel element to load content efficiently, avoiding the performance cost of continuous scroll event listeners.
- **Integration** requires invoking the animation check function after rendering new batches to ensure immediate visual feedback for dynamically loaded content.
- The jisan-mia/dom-projects repository validates these patterns through working implementations in `projects/animation-on-scroll/` and references in [`projects/question-list-progress/data.js`](https://github.com/jisan-mia/dom-projects/blob/main/projects/question-list-progress/data.js).

## Frequently Asked Questions

### How do I prevent scroll animation jank on low-performance devices?

Use **CSS transform** and **opacity** exclusively for animations, as these properties leverage the compositor thread without forcing layout recalculations. Avoid animating **width**, **height**, **margin**, or **top** properties, which trigger expensive reflows. Add `{ passive: true }` to scroll listeners to prevent blocking the main thread: `window.addEventListener('scroll', checkAndShowBoxes, { passive: true })`.

### Can I use these patterns with React or Vue components?

Yes. Initialize the IntersectionObserver inside a `useEffect` (React) or `mounted` (Vue) hook, storing references to the feed and sentinel elements. Disconnect the observer in the cleanup function to prevent memory leaks. For scroll animations, query the DOM after each render cycle or use a ref callback to track dynamically loaded elements.

### Why does my infinite scroll trigger multiple times rapidly?

This occurs when the sentinel remains visible after loading new content. Implement a **loading lock** using a boolean flag (e.g., `let isLoading = false`) that prevents concurrent fetch operations. Set the flag to `true` when loading starts and `false` after appending content. Alternatively, temporarily **unobserve** the sentinel before fetching and **re-observe** after the DOM updates.

### What is the performance difference between scroll events and IntersectionObserver?

Scroll events fire at the browser's refresh rate (60–120 times per second), executing JavaScript on every frame regardless of element position changes. IntersectionObserver uses the browser's native visibility engine, firing callbacks only when elements cross specified thresholds. For infinite scroll implementations, IntersectionObserver reduces CPU usage by approximately 90% compared to scroll event listeners with `getBoundingClientRect()` position checks.