DEV3 Technologies home

Reduced-motion aware scroll reveal

Fade elements in as they enter the viewport with an IntersectionObserver, staggered delays and a full opt-out for reduced motion.

WordPressAnimationscroll-revealintersection-observerreduced-motiona11y

Live preview

Code

/**
 * Reveal on scroll. Markup: <div data-reveal data-reveal-delay="120">...</div>
 * Without JavaScript the elements stay visible because the hiding class is added here.
 */
(() => {
	const items = document.querySelectorAll('[data-reveal]');
	if (!items.length) return;

	const reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
	if (reduce.matches || !('IntersectionObserver' in window)) return; // Leave everything visible.

	document.documentElement.classList.add('reveal-ready');

	const io = new IntersectionObserver((entries) => {
		entries.forEach((entry) => {
			if (!entry.isIntersecting) return;
			const el = entry.target;
			el.style.transitionDelay = `${Number(el.dataset.revealDelay) || 0}ms`;
			el.classList.add('is-visible');
			io.unobserve(el); // Animate once.
		});
	}, { threshold: 0.15, rootMargin: '0px 0px -8% 0px' });

	items.forEach((el) => io.observe(el));

	// If the user switches reduced motion on later, show everything now.
	reduce.addEventListener('change', () => {
		if (reduce.matches) items.forEach((el) => el.classList.add('is-visible'));
	});
})();

How it works

What it solves

Scroll animations add polish but can cause discomfort for people with vestibular disorders, and hidden content is a problem if scripts fail. This version only hides elements when the script is running and motion is allowed.

How it works

The script first checks prefers-reduced-motion and feature support; if either rules animation out it returns and nothing is ever hidden. Otherwise it adds a class to the root, which is what makes the CSS hide the elements, then observes each one. When an element is about 15 percent visible it receives is-visible, an optional per-element delay for staggering, and is unobserved so it only animates once.

A media query in the stylesheet is a second safeguard.

Adapting it

Add variants with attributes such as data-reveal="left" and matching CSS. Reduce the offset on small screens. For long lists, stagger by index in script rather than hand-written delays. In WordPress, enqueue both files and add the attribute to blocks through the Advanced section.

Pitfalls

  • Hiding content with CSS alone, without the ready class, leaves it invisible when scripts fail.
  • Do not animate large layout properties; transform and opacity are cheap.
  • Elements already in view on load still animate, which is intended.
  • Very tall elements may never reach the threshold; lower it for them.

How to use it

Load reveal.css in the head and reveal.js with defer.

Add data-reveal to any element, and data-reveal-delay="150" to stagger siblings.

Turn on reduced motion in your operating system to confirm everything stays visible.

Licence: GPL-2.0-or-later. This snippet comes from WordPress code and is GPL-2.0-or-later.

Related snippets