Scroll Animation with IntersectionObserver (No jQuery)

December 17, 2025
Scroll Animation with IntersectionObserver (No jQuery)

Scroll-triggered animations can make a WordPress site feel polished and modern — but you don’t need jQuery or heavy animation libraries. The browser-native IntersectionObserver API lets you detect when elements enter the viewport and toggle CSS classes efficiently.

This guide shows a clean, reusable pattern for scroll animations using IntersectionObserver + CSS transitions, with practical examples you can drop into any theme.


Why IntersectionObserver Is Better Than Scroll Events

  • No manual scroll listeners running every frame
  • More efficient and battery-friendly
  • Works well with lazy-loaded content
  • Easy to maintain (class toggle approach)

In short: it’s the modern way to do scroll-based UI effects.


1) Minimal HTML Markup

Add a class like js-reveal to elements you want to animate.

<section class="feature js-reveal">
  <h2>Fast, lightweight animations</h2>
  <p>Triggered only when the element enters the viewport.</p>
</section>

<div class="card js-reveal">
  <h3>Card Title</h3>
  <p>This card fades up when visible.</p>
</div>

2) CSS: Define the “Before” and “Visible” States

Use opacity + translate for a smooth “fade up” reveal.

.js-reveal {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 600ms ease, transform 600ms ease;
  will-change: opacity, transform;
}

.js-reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}

This keeps animation logic in CSS and JavaScript only toggles a class.


3) JavaScript: IntersectionObserver (Reusable)

This script watches all .js-reveal elements and adds .is-visible when they enter the viewport.

document.addEventListener('DOMContentLoaded', () => {
  const targets = document.querySelectorAll('.js-reveal');

  if (!targets.length) return;

  const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  if (prefersReducedMotion) {
    targets.forEach(el => el.classList.add('is-visible'));
    return;
  }

  const observer = new IntersectionObserver((entries, obs) => {
    entries.forEach(entry => {
      if (!entry.isIntersecting) return;

      entry.target.classList.add('is-visible');
      obs.unobserve(entry.target);
    });
  }, {
    root: null,
    rootMargin: '0px 0px -10% 0px',
    threshold: 0.1,
  });

  targets.forEach(el => observer.observe(el));
});

What the Options Mean

  • threshold: 0.1 → triggers when ~10% of the element is visible
  • rootMargin: '0px 0px -10% 0px' → triggers slightly before the element is fully in view
  • unobserve() → animates once (recommended for performance)

4) Stagger Animations (No Library)

You can stagger multiple items by setting a CSS variable per element.

HTML

<ul class="grid">
  <li class="js-reveal" style="--delay: 0ms;">Item 1</li>
  <li class="js-reveal" style="--delay: 80ms;">Item 2</li>
  <li class="js-reveal" style="--delay: 160ms;">Item 3</li>
</ul>

CSS

.js-reveal {
  opacity: 0;
  transform: translateY(16px);
  transition:
    opacity 600ms ease var(--delay, 0ms),
    transform 600ms ease var(--delay, 0ms);
}

The same IntersectionObserver script works — delay is handled purely in CSS.


5) Different Animation Types (Scale, Fade, Slide)

Use modifier classes to change the effect without touching JavaScript.

HTML

<div class="js-reveal reveal-fade">Fade only</div>
<div class="js-reveal reveal-slide-left">Slide from left</div>
<div class="js-reveal reveal-zoom">Zoom in</div>

CSS

.reveal-fade {
  transform: none;
}

.reveal-slide-left {
  transform: translateX(-16px);
}

.reveal-zoom {
  transform: scale(0.96);
}

.js-reveal.is-visible {
  opacity: 1;
  transform: translateX(0) translateY(0) scale(1);
}

Keep the “visible” state consistent and let the starting state vary.


6) Where to Add This in WordPress

Typical approach:

  • Put JS in /assets/js/reveal.js
  • Enqueue only where needed (or globally if used site-wide)
  • Add CSS to your main stylesheet or a dedicated animation file

Enqueue Example

add_action( 'wp_enqueue_scripts', function() {

  $path = get_stylesheet_directory() . '/assets/js/reveal.js';

  wp_enqueue_script(
    'reveal-io',
    get_stylesheet_directory_uri() . '/assets/js/reveal.js',
    array(),
    file_exists( $path ) ? filemtime( $path ) : null,
    true
  );

} );

Common Mistakes to Avoid

  • Animating layout properties like top, left, height (use transform instead)
  • Forgetting prefers-reduced-motion (accessibility)
  • Observing too many elements with overly complex animations
  • Not unobserving when you only need a one-time reveal

Conclusion

IntersectionObserver is the cleanest way to build scroll-triggered animations without jQuery. By toggling a single class and keeping the animation logic in CSS, you get a solution that’s fast, accessible, and easy to scale across a WordPress site.

Key takeaway:
Use IntersectionObserver to toggle .is-visible, and let CSS handle the animation.

Avatar

Written by

satoshi

I’ve been building and customizing WordPress themes for over 10 years. In my free time, you’ll probably find me enjoying a good football match.