Scroll Animation Trigger
UI elements that change color, size, or shape based on scroll progress. Text that reveals dynamically when entering the viewport.
Scroll Animation Magic
Discover beautiful animations triggered by your scrolling journey
Fade In Effect
This content gently fades into view as you scroll down the page, creating a subtle and elegant appearance that draws attention without being distracting.
Scale Effect
Watch as this content smoothly scales from small to full size as you scroll, creating a dynamic entrance that captures attention and adds visual depth.
Slide Up Effect
This content slides gracefully into view from below, creating a smooth transition that guides the eye naturally as you explore the page content.
Custom Animation
This demonstrates a custom animation path combining multiple movements and rotations. Create your own unique entrance effects with complete creative freedom.
Color Change
Watch the text transform through vibrant colors as you scroll through this section, creating a playful and engaging visual experience tied to your scroll position.
Rotation Effect
This content spins into place as you scroll, adding a dynamic and playful element to the page that catches the eye.
Installation Guide
Install Dependencies
Framer Motion
npm install framer-motion
Utility Functions
npm install clsx tailwind-merge
Setup Configuration
/lib/utils.ts
1import { clsx, type ClassValue } from "clsx";
2import { twMerge } from "tailwind-merge";
3
4 export function cn(...inputs: ClassValue[]) {
5 return twMerge(clsx(inputs));
6 }
Copy Component Code
1"use client";
2import { useRef, useEffect, useState, type ReactNode } from "react";
3import {
4 motion,
5 useScroll,
6 useTransform,
7 type MotionValue,
8} from "framer-motion";
9import { cn } from "@/lib/utils";
10
11export interface ScrollAnimationTriggerProps {
12 children: ReactNode;
13 className?: string;
14 effect?: "fade" | "scale" | "slide" | "color" | "rotate" | "custom";
15 threshold?: number;
16 delay?: number;
17 duration?: number;
18 direction?: "up" | "down" | "left" | "right";
19 once?: boolean;
20
21 // eslint-disable-next-line @typescript-eslint/no-explicit-any
22 customProps?: Record<string, any>;
23 as?: React.ElementType;
24 fromColor?: string;
25 toColor?: string;
26 fromRotation?: number;
27 toRotation?: number;
28 fromScale?: number;
29 toScale?: number;
30}
31
32export function ScrollAnimationTrigger({
33 children,
34 className,
35 effect = "fade",
36 threshold = 0.1,
37 delay = 0,
38 duration = 0.5,
39 direction = "up",
40 once = false,
41 customProps = {},
42 as = "div",
43 fromColor = "var(--color-muted)",
44 toColor = "var(--color-primary)",
45 fromRotation = direction === "left" ? -10 : 10,
46 toRotation = 0,
47 fromScale = 0.8,
48 toScale = 1,
49}: ScrollAnimationTriggerProps) {
50 const ref = useRef<HTMLDivElement>(null);
51 const [isInView, setIsInView] = useState(false);
52
53 const { scrollYProgress } = useScroll({
54 target: ref,
55 offset: ["start end", "end start"],
56 });
57
58 const textColor = useTransform(scrollYProgress, [0, 1], [fromColor, toColor]);
59 const rotation = useTransform(
60 scrollYProgress,
61 [0, 1],
62 [fromRotation, toRotation]
63 );
64
65 useEffect(() => {
66 if (!ref.current) return;
67 const observer = new IntersectionObserver(
68 (entries) => {
69 const [entry] = entries;
70 if (entry.isIntersecting) {
71 setIsInView(true);
72 if (once) observer.disconnect();
73 } else if (!once) {
74 setIsInView(false);
75 }
76 },
77 { threshold }
78 );
79
80 observer.observe(ref.current);
81 return () => observer.disconnect();
82 }, [threshold, once]);
83
84 const getAnimationProps = () => {
85 // eslint-disable-next-line @typescript-eslint/no-explicit-any
86 const baseProps: any = {
87 initial: {},
88 animate: {},
89 transition: { duration, delay, ease: "easeOut" },
90 };
91
92 switch (effect) {
93 case "fade":
94 baseProps.initial = { opacity: 0 };
95 baseProps.animate = isInView ? { opacity: 1 } : { opacity: 0 };
96 break;
97 case "scale":
98 baseProps.initial = { scale: fromScale, opacity: 0 };
99 baseProps.animate = isInView
100 ? { scale: toScale, opacity: 1 }
101 : { scale: fromScale, opacity: 0 };
102 break;
103 case "slide":
104 const offset = 50;
105 const directionMap = {
106 up: { y: offset },
107 down: { y: -offset },
108 left: { x: offset },
109 right: { x: -offset },
110 };
111 baseProps.initial = { ...directionMap[direction], opacity: 0 };
112 baseProps.animate = isInView
113 ? { x: 0, y: 0, opacity: 1 }
114 : { ...directionMap[direction], opacity: 0 };
115 break;
116 case "color":
117 baseProps.style = { color: textColor };
118 break;
119 case "rotate":
120 baseProps.style = { rotate: rotation, opacity: isInView ? 1 : 0 };
121 break;
122 case "custom":
123 return {
124 ...baseProps,
125 ...customProps,
126 animate: isInView
127 ? { ...customProps.animate }
128 : { ...customProps.initial },
129 };
130 default:
131 break;
132 }
133 return baseProps;
134 };
135
136 const MotionComponent =
137 as === "div"
138 ? motion.div
139 : as === "span"
140 ? motion.span
141 : as === "p"
142 ? motion.p
143 : as === "h1"
144 ? motion.h1
145 : as === "h2"
146 ? motion.h2
147 : as === "h3"
148 ? motion.h3
149 : as === "h4"
150 ? motion.h4
151 : as === "h5"
152 ? motion.h5
153 : as === "h6"
154 ? motion.h6
155 : as === "section"
156 ? motion.section
157 : as === "article"
158 ? motion.article
159 : as === "aside"
160 ? motion.aside
161 : as === "nav"
162 ? motion.nav
163 : as === "ul"
164 ? motion.ul
165 : as === "ol"
166 ? motion.ol
167 : as === "li"
168 ? motion.li
169 : as === "button"
170 ? motion.button
171 : motion.div;
172
173 return (
174 <MotionComponent
175 ref={ref}
176 className={cn("scroll-animation-trigger", className)}
177 {...getAnimationProps()}
178 >
179 {children}
180 </MotionComponent>
181 );
182}
183
184export function useScrollProgress(options = {}) {
185 const ref = useRef<HTMLDivElement>(null);
186 const { scrollYProgress } = useScroll({
187 target: ref,
188 offset: ["start end", "end start"],
189 ...options,
190 });
191 return { ref, scrollYProgress };
192}
193
194export function useScrollColor(
195 scrollYProgress: MotionValue<number>,
196 fromColor: string,
197 toColor: string
198) {
199 return useTransform(scrollYProgress, [0, 1], [fromColor, toColor]);
200}
201
202export function useScrollSize(
203 scrollYProgress: MotionValue<number>,
204 fromSize: number,
205 toSize: number
206) {
207 return useTransform(scrollYProgress, [0, 1], [fromSize, toSize]);
208}
209
210export function useScrollRotation(
211 scrollYProgress: MotionValue<number>,
212 fromRotation: number,
213 toRotation: number
214) {
215 return useTransform(scrollYProgress, [0, 1], [fromRotation, toRotation]);
216}
217
218export interface ScrollProgressAnimationProps {
219 children:
220 | ReactNode
221 | ((props: { scrollYProgress: MotionValue<number> }) => ReactNode);
222 className?: string;
223 offset?: ["start end", "end start"] | [string, string];
224}
225
226export function ScrollProgressAnimation({
227 children,
228 className,
229}: ScrollProgressAnimationProps) {
230 const ref = useRef<HTMLDivElement>(null);
231 const { scrollYProgress } = useScroll({
232 target: ref,
233 offset: ["start end", "end start"],
234 });
235
236 return (
237 <div ref={ref} className={cn("scroll-progress-animation", className)}>
238 {typeof children === "function"
239 ? children({ scrollYProgress })
240 : children}
241 </div>
242 );
243}
244
Final Steps
Update Import Paths
Make sure to update the import paths in the component code to match your project structure. For example, change @/components/ui/button
to match your UI components location.
Props
Name | Type | Default | Description |
---|---|---|---|
children | ReactNode | - | The content to be animated |
effect | string | fade | The animation effect - (fade, scale, slide, color, rotate, custom) |
threshold | number | 0.1 | The threshold for triggering the animation (0-1) |
delay | number | 0 | Delay before the animation starts (in seconds) |
duration | number | 0.5 | Duration of the animation (in seconds) |
direction | string | up | Direction for slide animation - (up, down, left, right) |
once | boolean | false | Whether to trigger the animation only once |
customProps | object | {} | Custom animation properties for the 'custom' effect |
as | React.ElementType | div | The element type to render - (div, span, etc) |
className | string | - | Additional CSS classes to apply |
fromColor | string | var(--color-muted) | Starting color for color transition |
toColor | string | var(--color-primary) | Ending color for color transition |
fromRotation | number | direction === "left" ? -10 : 10 | Initial rotation angle for rotate effect |
toRotation | number | 0 | Final rotation angle for rotate effect |
fromScale | number | 0.8 | Initial scale value for scale effect |
toScale | number | 1 | Final scale value for scale effect |
Examples
Fade In Animation
This content smoothly fades into view as it enters the viewport, creating a clean and elegant transition that enhances the user experience.
Slide Up Animation
This content gracefully slides up into position as you scroll, creating a natural flow that guides the user through your content.
Custom Animation
This content uses complex custom animations with diagonal movement, rotation, and scaling for maximum visual impact.
Scale Animation
Watch as this content smoothly scales from small to full size as you scroll, creating an eye-catching effect that emphasizes important information.
Color Change Animation
This content transforms through a beautiful color transition as it enters your view, creating a visually engaging experience.
Slide From Left
This panel slides in from the left side of the screen.
Slide From Right
This panel slides in from the right side of the screen.
Scale Up
This panel scales up from a smaller size.
Fade In
This panel fades in after all others appear.