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'));
});
})();
.reveal-ready [data-reveal] {
opacity: 0;
transform: translateY(24px);
transition: opacity .6s ease, transform .6s cubic-bezier(.2, .7, .2, 1);
}
.reveal-ready [data-reveal].is-visible {
opacity: 1;
transform: none;
}
@media (prefers-reduced-motion: reduce) {
[data-reveal] { opacity: 1 !important; transform: none !important; transition: none !important; }
}
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.