45 lines
1.8 KiB
TypeScript
45 lines
1.8 KiB
TypeScript
/* Reveal-on-scroll for browsers without scroll-driven timelines.
|
|
----------------------------------------------------------------------------
|
|
motion.css handles `.sh-reveal` with `animation-timeline: view()`, which is
|
|
Chromium-only; elsewhere every band sits at rest, fully visible. This helper
|
|
gives those browsers the same `.sh-rise` reveal: it tags each `.sh-reveal`
|
|
element `.sh-reveal-js` (hidden), then flips it to `.sh-reveal-js--in` (the
|
|
animation runs) once it scrolls into view.
|
|
|
|
It is a progressive enhancement and is strictly opt-in per page:
|
|
- Chromium already animates via CSS, so this returns early and never
|
|
fights the scroll-driven version.
|
|
- Reduced-motion visitors are left alone (the CSS never hides them).
|
|
- With JS off, no classes are ever added, so content stays visible.
|
|
|
|
Renders nothing on the server; call from a `useEffect` in the page. */
|
|
|
|
const SELECTOR = ".sh-reveal";
|
|
|
|
export function enableReveals(): void {
|
|
if (typeof window === "undefined") return;
|
|
if (!("IntersectionObserver" in window)) return;
|
|
if ("CSS" in window && CSS.supports("animation-timeline: view()")) {
|
|
/* Chromium already plays the reveal; nothing to add. */
|
|
return;
|
|
}
|
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
for (const entry of entries) {
|
|
if (!entry.isIntersecting) continue;
|
|
const element = entry.target;
|
|
element.classList.add("sh-reveal-js--in");
|
|
element.classList.remove("sh-reveal-js");
|
|
observer.unobserve(element);
|
|
}
|
|
},
|
|
{ rootMargin: "0px 0px -8% 0px", threshold: 0.05 },
|
|
);
|
|
|
|
for (const element of document.querySelectorAll<HTMLElement>(SELECTOR)) {
|
|
element.classList.add("sh-reveal-js");
|
|
observer.observe(element);
|
|
}
|
|
} |