Expert knowledge for scroll-triggered animations, parallax effects, and scroll-linked interactions that create immersive storytelling experiences.
From react-animation-studionpx claudepluginhub markus41/claude --plugin react-animation-studioThis skill uses the workspace's default tool permissions.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Configures VPN and dedicated connections like Direct Connect, ExpressRoute, Interconnect for secure on-premises to AWS, Azure, GCP, OCI hybrid networking.
Expert knowledge for scroll-triggered animations, parallax effects, and scroll-linked interactions that create immersive storytelling experiences.
Activate this skill when:
**/*.tsx with scroll-related hooks**/hooks/useScroll*.tsScrollTrigger importsuseScroll, useInView from framer-motionimport { motion, useScroll, useTransform, useInView } from 'framer-motion';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
const observer = new IntersectionObserver(callback, options);
import { motion, useScroll, useTransform } from 'framer-motion';
function ScrollProgress() {
const { scrollYProgress } = useScroll();
return (
<motion.div
className="fixed top-0 left-0 h-1 bg-blue-500 origin-left"
style={{ scaleX: scrollYProgress }}
/>
);
}
function Parallax() {
const { scrollY } = useScroll();
// Background moves slower than foreground
const backgroundY = useTransform(scrollY, [0, 1000], [0, -200]);
const foregroundY = useTransform(scrollY, [0, 1000], [0, -400]);
return (
<div className="relative h-screen overflow-hidden">
<motion.div
className="absolute inset-0 bg-cover bg-center"
style={{ y: backgroundY, backgroundImage: 'url(bg.jpg)' }}
/>
<motion.div
className="relative z-10"
style={{ y: foregroundY }}
>
<h1>Foreground Content</h1>
</motion.div>
</div>
);
}
function ElementParallax() {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start end', 'end start'], // When to start/end tracking
});
const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [0, 1, 0]);
const y = useTransform(scrollYProgress, [0, 1], [100, -100]);
return (
<motion.div
ref={ref}
style={{ opacity, y }}
className="h-screen flex items-center justify-center"
>
Content that fades and moves with scroll
</motion.div>
);
}
import { motion, useInView } from 'framer-motion';
function RevealOnScroll({ children }: { children: React.ReactNode }) {
const ref = useRef(null);
const isInView = useInView(ref, {
once: true, // Only trigger once
margin: '-100px', // Trigger 100px before entering viewport
});
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 50 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, ease: 'easeOut' }}
>
{children}
</motion.div>
);
}
import { useGSAP } from '@gsap/react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
function Component() {
const containerRef = useRef(null);
useGSAP(() => {
gsap.from('.reveal-item', {
y: 100,
opacity: 0,
duration: 1,
stagger: 0.2,
scrollTrigger: {
trigger: '.reveal-container',
start: 'top 80%',
end: 'bottom 20%',
toggleActions: 'play none none reverse',
// markers: true, // Debug
},
});
}, { scope: containerRef });
return (
<div ref={containerRef}>
<div className="reveal-container">
<div className="reveal-item">Item 1</div>
<div className="reveal-item">Item 2</div>
<div className="reveal-item">Item 3</div>
</div>
</div>
);
}
useGSAP(() => {
gsap.to('.progress-bar', {
scaleX: 1,
ease: 'none',
scrollTrigger: {
trigger: '.content',
start: 'top top',
end: 'bottom bottom',
scrub: true, // Links animation to scroll position
},
});
});
useGSAP(() => {
const tl = gsap.timeline({
scrollTrigger: {
trigger: '.pin-section',
start: 'top top',
end: '+=300%', // 3x viewport height of scrolling
pin: true,
scrub: 1,
},
});
tl.to('.step-1', { opacity: 0 })
.to('.step-2', { opacity: 1 })
.to('.step-2', { opacity: 0 })
.to('.step-3', { opacity: 1 });
});
useGSAP(() => {
const panels = gsap.utils.toArray('.panel');
gsap.to(panels, {
xPercent: -100 * (panels.length - 1),
ease: 'none',
scrollTrigger: {
trigger: '.horizontal-container',
pin: true,
scrub: 1,
snap: 1 / (panels.length - 1),
end: () => '+=' + document.querySelector('.horizontal-container')!.offsetWidth,
},
});
});
import { useState, useEffect, useRef, RefObject } from 'react';
interface InViewOptions {
threshold?: number | number[];
rootMargin?: string;
triggerOnce?: boolean;
}
export function useInView<T extends HTMLElement>(
options: InViewOptions = {}
): [RefObject<T>, boolean] {
const { threshold = 0, rootMargin = '0px', triggerOnce = false } = options;
const ref = useRef<T>(null);
const [inView, setInView] = useState(false);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setInView(true);
if (triggerOnce) {
observer.unobserve(element);
}
} else if (!triggerOnce) {
setInView(false);
}
},
{ threshold, rootMargin }
);
observer.observe(element);
return () => observer.disconnect();
}, [threshold, rootMargin, triggerOnce]);
return [ref, inView];
}
// Usage
function Component() {
const [ref, inView] = useInView<HTMLDivElement>({ triggerOnce: true });
return (
<div
ref={ref}
className={`transition-all duration-500 ${
inView ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'
}`}
>
Content reveals when scrolled into view
</div>
);
}
function TextReveal() {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start end', 'end start'],
});
const words = 'This text reveals word by word'.split(' ');
return (
<p ref={ref} className="text-4xl font-bold">
{words.map((word, i) => {
const start = i / words.length;
const end = start + 1 / words.length;
const opacity = useTransform(
scrollYProgress,
[start, end],
[0.2, 1]
);
return (
<motion.span
key={i}
style={{ opacity }}
className="inline-block mr-2"
>
{word}
</motion.span>
);
})}
</p>
);
}
function Scroll3DCard() {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ['start end', 'center center'],
});
const rotateX = useTransform(scrollYProgress, [0, 1], [45, 0]);
const scale = useTransform(scrollYProgress, [0, 1], [0.8, 1]);
const opacity = useTransform(scrollYProgress, [0, 0.5], [0, 1]);
return (
<motion.div
ref={ref}
style={{
rotateX,
scale,
opacity,
transformPerspective: 1000,
}}
className="bg-white rounded-xl shadow-xl p-8"
>
Card content
</motion.div>
);
}
function SectionNavigator() {
const sections = ['intro', 'features', 'pricing', 'contact'];
const { scrollYProgress } = useScroll();
const activeIndex = useTransform(
scrollYProgress,
sections.map((_, i) => i / sections.length),
sections.map((_, i) => i)
);
return (
<nav className="fixed right-4 top-1/2 -translate-y-1/2">
{sections.map((section, i) => (
<motion.button
key={section}
className="block w-3 h-3 rounded-full my-2"
style={{
backgroundColor: useTransform(
activeIndex,
[i - 0.5, i, i + 0.5],
['#ccc', '#3b82f6', '#ccc']
),
}}
onClick={() => {
document.getElementById(section)?.scrollIntoView({ behavior: 'smooth' });
}}
/>
))}
</nav>
);
}
function FadeInCard({ children, delay = 0 }) { const ref = useRef(null); const isInView = useInView(ref, { once: true, margin: '-50px' });
return ( <motion.div ref={ref} initial={{ opacity: 0, y: 30 }} animate={isInView ? { opacity: 1, y: 0 } : {}} transition={{ duration: 0.5, delay, ease: 'easeOut' }} > {children} </motion.div> ); }
// Usage
<div className="grid grid-cols-3 gap-4"> {cards.map((card, i) => ( <FadeInCard key={card.id} delay={i * 0.1}> <Card>{card.content}</Card> </FadeInCard> ))} </div> ``` </example> <example> Context: User needs a parallax hero section user: "Create a parallax scrolling effect for the hero" assistant: Using scroll-linked transforms for parallax: ```tsx function ParallaxHero() { const { scrollY } = useScroll();const backgroundY = useTransform(scrollY, [0, 500], [0, 150]); const textY = useTransform(scrollY, [0, 500], [0, -100]); const opacity = useTransform(scrollY, [0, 300], [1, 0]);
return ( <div className="relative h-screen overflow-hidden"> <motion.img src="/hero-bg.jpg" className="absolute inset-0 w-full h-full object-cover" style={{ y: backgroundY }} /> <motion.div className="relative z-10 flex items-center justify-center h-full" style={{ y: textY, opacity }} > <h1 className="text-6xl font-bold text-white">Welcome</h1> </motion.div> </div> ); }
</example>
## Related Skills
- **framer-motion** - Core animation library
- **gsap** - Advanced scroll triggers
- **transition-engineer** - Page transitions
## Author
Created by Brookside BI as part of React Animation Studio