design/motion-design/skills/scroll-animation-builder/SKILL.md
This skill should be used when the user wants animations that are triggered by or linked to scroll position. Trigger when the user mentions "scroll animation", "scroll-triggered", "on scroll", "scroll reveal", "fade in on scroll", "parallax", "horizontal scroll", "sticky section", "scroll-linked animation", "pin section on scroll", "scroll progress", "Intersection Observer", "GSAP ScrollTrigger", "useScroll Framer Motion", or wants elements to animate as the user scrolls. Also trigger when the user asks about "hero animation", "scroll storytelling", or mentions a landing page needs to feel "alive while scrolling".
npx skillsauth add harsh040506/claude-code-unified-skill-plugin-library scroll-animation-builderInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Scroll animations come in two categories with fundamentally different implementation approaches:
useScroll + useTransform or GSAP ScrollTrigger.Choosing the wrong type for a scenario is the single most common scroll animation mistake.
| When to use scroll-triggered | When to use scroll-linked | |------------------------------|---------------------------| | Element should animate in once and stay | Animation progress mirrors scroll position | | Content reveals as user reads down | Parallax depth effects | | Performance is critical (runs once) | Sticky section transitions | | "Reveal on scroll" pattern | Scroll progress indicators | | Most content sections in a product page | Hero storytelling sequences |
Why IntersectionObserver: It fires only when elements enter/exit the viewport and runs off the main thread — zero scroll event listener overhead, no debouncing needed.
// Vanilla JS — most performant
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("is-visible");
observer.unobserve(entry.target); // Only animate in once
}
});
},
{
threshold: 0.15, // Trigger when 15% of element is visible
rootMargin: "0px", // No offset — trigger exactly at viewport edge
}
);
document.querySelectorAll("[data-animate]").forEach((el) => observer.observe(el));
[data-animate] {
opacity: 0;
transform: translateY(16px);
transition: opacity 280ms ease-out, transform 280ms ease-out;
}
/* Stagger siblings — CSS only, no JS needed */
[data-animate]:nth-child(2) { transition-delay: 60ms; }
[data-animate]:nth-child(3) { transition-delay: 120ms; }
[data-animate]:nth-child(4) { transition-delay: 180ms; }
[data-animate].is-visible {
opacity: 1;
transform: translateY(0);
}
Never animate elements already in the viewport on page load. This punishes fast connections and makes the page look broken. Check the element's initial position before observing.
Framer Motion variant — whileInView:
<motion.div
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.15 }}
transition={{ duration: 0.28, ease: [0, 0, 0.58, 1] }}
>
Content
</motion.div>
Staggered list reveal:
function AnimatedList({ items }) {
const container = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.06,
delayChildren: 0.1,
},
},
};
const item = {
hidden: { opacity: 0, y: 12 },
visible: { opacity: 1, y: 0, transition: { duration: 0.26, ease: [0, 0, 0.58, 1] } },
};
return (
<motion.ul variants={container} initial="hidden" whileInView="visible" viewport={{ once: true }}>
{items.map((i) => (
<motion.li key={i} variants={item}>{i}</motion.li>
))}
</motion.ul>
);
}
import { useScroll, useTransform, motion } from "framer-motion";
import { useRef } from "react";
function ParallaxSection() {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"], // Track while element is in viewport
});
// useTransform maps scroll progress (0→1) to animation values
const y = useTransform(scrollYProgress, [0, 1], [60, -60]); // Parallax
const opacity = useTransform(scrollYProgress, [0, 0.3, 0.7, 1], [0, 1, 1, 0]); // Fade in/out
return (
<section ref={ref}>
<motion.div style={{ y, opacity }}>
Parallax content
</motion.div>
</section>
);
}
Scroll progress indicator:
function ScrollProgress() {
const { scrollYProgress } = useScroll();
return (
<motion.div
className="scroll-progress-bar"
style={{ scaleX: scrollYProgress, transformOrigin: "left" }}
/>
);
}
Use GSAP when: multiple elements need precise choreography tied to scroll, you need pinning (keeping a section fixed while scrolling past it), or you're building a complex editorial/marketing sequence.
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
// Pin a section while content animates through it
gsap.timeline({
scrollTrigger: {
trigger: ".hero",
start: "top top",
end: "+=300%", // Pin for 3 viewport heights of scroll distance
pin: true, // Lock the trigger element in place
scrub: 1, // Animation tied to scroll (1s smoothing lag)
},
})
.from(".hero-title", { y: 60, opacity: 0 })
.from(".hero-subtitle", { y: 40, opacity: 0 }, "-=0.5")
.from(".hero-image", { scale: 0.9, opacity: 0 }, "-=0.3");
Horizontal scroll section:
gsap.to(".horizontal-track", {
xPercent: -100 * (sections - 1), // Scroll the full width
ease: "none",
scrollTrigger: {
trigger: ".horizontal-container",
pin: true,
scrub: 1,
end: () => "+=" + document.querySelector(".horizontal-track").offsetWidth,
},
});
Minimum 3 layers for convincing depth:
const { scrollY } = useScroll();
// Background moves slowest (0.3× scroll speed)
const bgY = useTransform(scrollY, [0, 1000], [0, -300]);
// Subject moves at 1× (normal scroll)
const subjectY = useTransform(scrollY, [0, 1000], [0, -100]);
// Foreground moves fastest (1.3× scroll speed)
const fgY = useTransform(scrollY, [0, 1000], [0, 130]);
Maximum displacement in product UIs: 15–20px total travel. Marketing pages can push to 60px. Beyond that, content becomes unreadable mid-scroll.
Always gate parallax on prefers-reduced-motion — it is the most common vestibular trigger in web UI.
Scroll-linked animations run on every scroll tick. Any performance mistake here produces visible jank because there is no idle time to recover.
transform and opacity — never top, left, width, or any layout propertywill-change: transform on elements with scroll-linked transforms — promotes to GPU layer before animation beginscontain: layout style paint on scroll animation containers — limits layout recalc scope{ passive: true } and throttle to requestAnimationFrame// Passive listener — never blocks scroll
window.addEventListener("scroll", handler, { passive: true });
// Only dispatch once per animation frame
let ticking = false;
function handler() {
if (!ticking) {
requestAnimationFrame(() => { updateAnimation(); ticking = false; });
ticking = true;
}
}
references/scroll-patterns.md — CSS animation-timeline: scroll() (native standard), SVG path animation on scroll, horizontal scroll galleries, scroll-triggered counters, and mobile-specific scroll considerationstesting
Performs quality control on single-cell RNA-seq data (.h5ad or .h5 files) using scverse best practices with MAD-based filtering and comprehensive visualizations. Use when users request QC analysis, filtering low-quality cells, assessing data quality, or following scverse/scanpy best practices for single-cell analysis.
tools
Deep learning for single-cell analysis using scvi-tools. This skill should be used when users need (1) data integration and batch correction with scVI/scANVI, (2) ATAC-seq analysis with PeakVI, (3) CITE-seq multi-modal analysis with totalVI, (4) multiome RNA+ATAC analysis with MultiVI, (5) spatial transcriptomics deconvolution with DestVI, (6) label transfer and reference mapping with scANVI/scArches, (7) RNA velocity with veloVI, or (8) any deep learning-based single-cell method. Triggers include mentions of scVI, scANVI, totalVI, PeakVI, MultiVI, DestVI, veloVI, sysVI, scArches, variational autoencoder, VAE, batch correction, data integration, multi-modal, CITE-seq, multiome, reference mapping, latent space.
testing
This skill should be used when scientists need help with research problem selection, project ideation, troubleshooting stuck projects, or strategic scientific decisions. Use this skill when users ask to pitch a new research idea, work through a project problem, evaluate project risks, plan research strategy, navigate decision trees, or get help choosing what scientific problem to work on. Typical requests include "I have an idea for a project", "I'm stuck on my research", "help me evaluate this project", "what should I work on", or "I need strategic advice about my research".
development
Run nf-core bioinformatics pipelines (rnaseq, sarek, atacseq) on sequencing data. Use when analyzing RNA-seq, WGS/WES, or ATAC-seq data—either local FASTQs or public datasets from GEO/SRA. Triggers on nf-core, Nextflow, FASTQ analysis, variant calling, gene expression, differential expression, GEO reanalysis, GSE/GSM/SRR accessions, or samplesheet creation.