Scroll tracking hooks and utilities for scroll-driven animations and viewport intersection detection.
Track scroll progress and position with reactive motion values.
/**
* Track scroll progress and position
* @param options - Scroll tracking options
* @returns Object containing scroll motion values
*/
function useScroll(options?: UseScrollOptions): ScrollMotionValues;
interface UseScrollOptions {
/**
* Element to track scroll within (default: window)
*/
container?: React.RefObject<Element>;
/**
* Target element to track relative to container
*/
target?: React.RefObject<Element>;
/**
* Offset ranges for when to start/end progress calculation
* Format: ["start start", "end end"] where each position can be:
* - "start", "center", "end" (of viewport)
* - Number (pixels from start)
* - Percentage string (e.g., "25%")
*/
offset?: [string, string];
/**
* Axis to track ("x" or "y", default: "y")
*/
axis?: "x" | "y";
}
interface ScrollMotionValues {
/**
* Horizontal scroll position in pixels
*/
scrollX: MotionValue<number>;
/**
* Vertical scroll position in pixels
*/
scrollY: MotionValue<number>;
/**
* Horizontal scroll progress (0-1)
*/
scrollXProgress: MotionValue<number>;
/**
* Vertical scroll progress (0-1)
*/
scrollYProgress: MotionValue<number>;
}Usage Examples:
import { motion, useScroll, useTransform } from "framer-motion";
import { useRef } from "react";
// Basic scroll tracking
function ScrollExample() {
const { scrollY, scrollYProgress } = useScroll();
const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [1, 0.5, 0]);
const scale = useTransform(scrollYProgress, [0, 1], [1, 0.8]);
return (
<motion.div
style={{ opacity, scale }}
className="fixed top-0 left-0 w-full h-20 bg-blue-500"
>
Header (opacity: {Math.round(opacity.get() * 100)}%)
</motion.div>
);
}
// Element-specific scroll tracking
function ElementScrollExample() {
const containerRef = useRef<HTMLDivElement>(null);
const targetRef = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: targetRef,
offset: ["start end", "end start"]
});
const x = useTransform(scrollYProgress, [0, 1], ["-100%", "0%"]);
return (
<div>
<div ref={containerRef} className="h-screen overflow-y-auto">
<div className="h-screen bg-gray-100">Scroll down</div>
<motion.div
ref={targetRef}
style={{ x }}
className="h-96 bg-blue-500"
>
Animates based on scroll position
</motion.div>
<div className="h-screen bg-gray-100">More content</div>
</div>
</div>
);
}
// Container scroll tracking
function ContainerScrollExample() {
const containerRef = useRef<HTMLDivElement>(null);
const { scrollY, scrollYProgress } = useScroll({
container: containerRef
});
const backgroundY = useTransform(scrollY, [0, 500], [0, -100]);
return (
<div ref={containerRef} className="h-96 overflow-y-auto">
<motion.div
style={{ y: backgroundY }}
className="h-[200vh] bg-gradient-to-b from-blue-400 to-purple-600"
>
Parallax background
</motion.div>
</div>
);
}Detect when elements enter or leave the viewport.
/**
* Detect when an element is in the viewport
* @param ref - Reference to element to observe
* @param options - Intersection options
* @returns Boolean indicating if element is in view
*/
function useInView(
ref: React.RefObject<Element>,
options?: UseInViewOptions
): boolean;
interface UseInViewOptions {
/**
* Root element for intersection calculation (default: viewport)
*/
root?: React.RefObject<Element>;
/**
* Root margin for intersection calculation (CSS margin syntax)
*/
margin?: string;
/**
* Amount of element that must be visible (0-1 or "some"/"all")
*/
amount?: number | "some" | "all";
/**
* Only trigger once when element comes into view
*/
once?: boolean;
/**
* Initial state before observation starts
*/
initial?: boolean;
}Usage Examples:
import { motion, useInView } from "framer-motion";
import { useRef } from "react";
// Basic in-view detection
function InViewExample() {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true });
return (
<div>
<div className="h-screen bg-gray-100">Scroll down</div>
<motion.div
ref={ref}
initial={{ opacity: 0, y: 100 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 100 }}
transition={{ duration: 0.8, ease: "easeOut" }}
className="h-96 bg-blue-500 flex items-center justify-center text-white"
>
Animates when in view
</motion.div>
</div>
);
}
// Advanced in-view options
function AdvancedInViewExample() {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, {
margin: "-100px",
amount: 0.3,
once: false
});
return (
<motion.div
ref={ref}
animate={{
scale: isInView ? 1 : 0.8,
opacity: isInView ? 1 : 0.5
}}
transition={{ duration: 0.6 }}
className="h-96 bg-green-500"
>
Scales when 30% visible (with 100px margin)
</motion.div>
);
}Direct DOM scroll utilities that work without React.
/**
* Track scroll with callback (DOM utility)
* @param onScroll - Callback function called on scroll
* @param options - Scroll options
* @returns Cleanup function
*/
function scroll(
onScroll: (info: ScrollInfo) => void,
options?: ScrollOptions
): () => void;
/**
* Advanced scroll tracking with detailed information
* @param onScroll - Callback function with detailed scroll info
* @param options - Scroll options
* @returns Cleanup function
*/
function scrollInfo(
onScroll: (info: DetailedScrollInfo) => void,
options?: ScrollOptions
): () => void;
interface ScrollOptions {
/**
* Element to observe (default: window)
*/
container?: Element;
/**
* Target element for relative calculations
*/
target?: Element;
/**
* Offset configuration
*/
offset?: [string, string];
/**
* Axis to track
*/
axis?: "x" | "y";
}
interface ScrollInfo {
/**
* Current scroll position
*/
x: number;
y: number;
/**
* Scroll progress (0-1)
*/
xProgress: number;
yProgress: number;
/**
* Scroll velocity
*/
velocity: number;
}
interface DetailedScrollInfo extends ScrollInfo {
/**
* Target element bounds
*/
targetBounds: DOMRect;
/**
* Container bounds
*/
containerBounds: DOMRect;
/**
* Intersection ratio
*/
intersectionRatio: number;
}Usage Examples:
import { scroll, scrollInfo } from "framer-motion/dom";
import { useEffect } from "react";
// Basic DOM scroll tracking
function DOMScrollExample() {
useEffect(() => {
const cleanup = scroll(
({ y, yProgress, velocity }) => {
console.log("Scroll:", { y, yProgress, velocity });
// Update header based on scroll
const header = document.querySelector('.header');
if (header) {
header.style.transform = `translateY(${Math.min(0, -y / 2)}px)`;
header.style.opacity = String(1 - yProgress * 0.5);
}
},
{ axis: "y" }
);
return cleanup;
}, []);
return (
<div>
<div className="header fixed top-0 w-full h-16 bg-blue-500">Header</div>
<div className="h-[200vh] pt-16">Long scrollable content</div>
</div>
);
}
// Element-specific DOM scroll tracking
function ElementDOMScrollExample() {
useEffect(() => {
const target = document.querySelector('.target');
if (target) {
const cleanup = scrollInfo(
({ yProgress, intersectionRatio, velocity }) => {
console.log("Element scroll:", { yProgress, intersectionRatio, velocity });
// Animate element based on scroll
target.style.transform = `scale(${0.8 + intersectionRatio * 0.2})`;
target.style.opacity = String(intersectionRatio);
},
{
target: target as Element,
offset: ["start end", "end start"]
}
);
return cleanup;
}
}, []);
return (
<div>
<div className="h-screen bg-gray-100">Scroll down</div>
<div className="target h-96 bg-purple-500">Target element</div>
<div className="h-screen bg-gray-100">More content</div>
</div>
);
}Direct DOM intersection observer utility.
/**
* Observe element intersection with viewport (DOM utility)
* @param element - Element to observe or CSS selector
* @param onStart - Callback when element enters view
* @param options - Intersection options
* @returns Cleanup function
*/
function inView(
element: string | Element,
onStart: (entry: IntersectionObserverEntry) => void | (() => void),
options?: InViewOptions
): () => void;
interface InViewOptions {
/**
* Root element for intersection
*/
root?: Element;
/**
* Root margin
*/
margin?: string;
/**
* Intersection threshold (0-1 or array of thresholds)
*/
amount?: number | number[] | "some" | "all";
}Usage Example:
import { inView } from "framer-motion/dom";
import { useEffect } from "react";
function DOMInViewExample() {
useEffect(() => {
// Observe multiple elements
const elements = document.querySelectorAll('.animate-on-scroll');
const cleanups: (() => void)[] = [];
elements.forEach((element, index) => {
const cleanup = inView(
element,
(entry) => {
if (entry.isIntersecting) {
// Element entered view
element.classList.add('animate-in');
} else {
// Element left view
element.classList.remove('animate-in');
}
},
{
margin: "-50px",
amount: 0.5
}
);
cleanups.push(cleanup);
});
return () => {
cleanups.forEach(cleanup => cleanup());
};
}, []);
return (
<div>
{Array.from({ length: 10 }, (_, i) => (
<div
key={i}
className="animate-on-scroll h-96 bg-blue-500 m-4 opacity-0 transform translate-y-10 transition-all duration-500"
style={{
// CSS for animate-in class
'--animate-in': 'opacity: 1; transform: translateY(0);'
}}
>
Element {i + 1}
</div>
))}
</div>
);
}Detect when the page becomes visible or hidden.
/**
* Detect page visibility changes
* @returns Boolean indicating if page is visible
*/
function usePageInView(): boolean;Usage Example:
import { usePageInView } from "framer-motion";
import { useEffect } from "react";
function PageVisibilityExample() {
const isPageVisible = usePageInView();
useEffect(() => {
if (isPageVisible) {
console.log("Page became visible");
// Resume animations or video playback
} else {
console.log("Page became hidden");
// Pause animations or video playback
}
}, [isPageVisible]);
return (
<div>
Page is {isPageVisible ? "visible" : "hidden"}
</div>
);
}Parallax Effects:
const { scrollY } = useScroll();
const y1 = useTransform(scrollY, [0, 1000], [0, -200]);
const y2 = useTransform(scrollY, [0, 1000], [0, -400]);
<motion.div style={{ y: y1 }}>Slow parallax</motion.div>
<motion.div style={{ y: y2 }}>Fast parallax</motion.div>Progress Indicators:
const { scrollYProgress } = useScroll();
const scaleX = useTransform(scrollYProgress, [0, 1], [0, 1]);
<motion.div
style={{ scaleX }}
className="fixed top-0 left-0 w-full h-1 bg-blue-500 origin-left"
/>Scroll-Triggered Animations:
const { scrollY } = useScroll();
const isScrolled = useTransform(scrollY, [0, 100], [false, true]);
<motion.header
animate={isScrolled.get() ? "scrolled" : "top"}
variants={{
top: { backgroundColor: "transparent", y: 0 },
scrolled: { backgroundColor: "white", y: -10 }
}}
/>