js-animation
JS Animation Skill
Add production-quality JavaScript animations to any web project — landing pages, presentations, dashboards, interactive demos, or marketing sites. This skill guides library selection based on audience and context, then provides complete, copy-paste code recipes organized by animation category.
Core Philosophy
- Purpose-Driven Motion — Every animation must serve a purpose: guide attention, convey hierarchy, provide feedback, or create delight. Never animate for the sake of animating.
- Audience-Aware — A corporate executive deck needs different motion than a creative portfolio. Always match animation intensity to the audience.
- Performance First — Animate only GPU-composited properties (
transform,opacity). Respectprefers-reduced-motion. CapdevicePixelRatio. Use a single RAF loop. - CDN-Loadable — All libraries must be loadable via
<script>tags from CDNs. No npm, no build tools, no bundlers required. - Progressive Enhancement — Animations enhance content, never gate it. The page must be fully readable and functional with JS disabled or animations turned off.
Phase 0: Detect Mode
Determine what the user needs:
Mode A: Add Animations to Existing Page
- User has an HTML file and wants to enhance it with animations
- Read the existing file, identify animation opportunities
- Proceed to Phase 1
Mode B: Build Animated Page from Scratch
- User wants to create a new animated page/component
- Gather requirements for content and animation style
- Proceed to Phase 1
Mode C: Animation Research / Consulting
- User wants recommendations on libraries, techniques, or approaches
- Skip to relevant Quick Reference sections
- Provide tailored advice
Phase 1: Context Discovery
Use AskUserQuestion to understand the project:
Step 1.1: Target Audience
Question 1: Audience
- Header: "Audience"
- Question: "Who will view this animated page?"
- Options:
- "Corporate / Executive" -- Board decks, investor pitches, enterprise dashboards
- "Developer / Technical" -- Developer tools, API docs, technical demos
- "Creative / Design" -- Portfolios, agency sites, award-worthy experiences
- "Marketing / Sales" -- Landing pages, product launches, conversion-focused
- multiSelect: false
Step 1.2: Animation Scope
Question 2: What to Animate
- Header: "Scope"
- Question: "What kinds of animations do you need?"
- Options:
- "Text & scroll effects" -- Heading reveals, word-by-word, scroll-triggered
- "3D / WebGL backgrounds" -- Particle meshes, floating objects, shaders
- "Full page experience" -- Scroll-driven narrative with pinning, parallax, transitions
- "Micro-interactions only" -- Hover effects, toggles, subtle motion
- multiSelect: true
Step 1.3: Existing Libraries
Question 3: Current Setup
- Header: "Libraries"
- Question: "Are any animation libraries already loaded?"
- Options:
- "None yet" -- Starting fresh
- "GSAP" -- GreenSock already loaded
- "Three.js" -- Three.js already loaded
- "Other / not sure" -- Something else or unknown
Phase 2: Library Selection
Based on Phase 1 answers, recommend one of these CDN stacks:
Minimal Stack (~40kb) -- For text & scroll effects
Best for: Corporate, Educational, Marketing pages with scroll-triggered reveals.
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14/dist/SplitText.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/rough-notation@0.5/lib/rough-notation.iife.js"></script>
Full-Featured Stack (~80kb) -- For rich scroll experiences
Best for: Marketing, Developer, Educational pages with SVG drawing, layout animations, smooth scroll.
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14/dist/ScrollSmoother.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14/dist/SplitText.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14/dist/Flip.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14/dist/DrawSVGPlugin.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14/dist/MorphSVGPlugin.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/rough-notation@0.5/lib/rough-notation.iife.js"></script>
<script src="https://cdn.jsdelivr.net/npm/countup.js@2/dist/countUp.umd.js"></script>
3D-Enhanced Stack (~240kb) -- For WebGL + scroll
Best for: Creative portfolios, agency sites, award-worthy experiences.
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14/dist/ScrollSmoother.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14/dist/SplitText.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14/dist/DrawSVGPlugin.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/rough-notation@0.5/lib/rough-notation.iife.js"></script>
Lightweight Alternative Stack (~15kb) -- Anime.js-based
Best for: When GSAP feels too heavy or you want a different API style.
<script src="https://cdn.jsdelivr.net/npm/animejs@4.3/dist/bundles/anime.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/rough-notation@0.5/lib/rough-notation.iife.js"></script>
Phase 3: Implementation — Animation Recipes
3A. Text Animations
Split Text — Character Stagger Reveal (GSAP SplitText)
The most impactful text animation. Characters fly in with rotation and bounce.
// Register plugins
gsap.registerPlugin(ScrollTrigger, SplitText);
// Split heading into characters
const heading = document.querySelector('.animate-heading');
const split = new SplitText(heading, { type: 'chars,words' });
// Animate characters in with stagger
gsap.from(split.chars, {
opacity: 0,
y: 60,
rotateX: -40,
stagger: 0.03,
duration: 0.8,
ease: 'back.out(1.7)',
scrollTrigger: {
trigger: heading,
start: 'top 85%',
toggleActions: 'play none none none',
}
});
Word-by-Word Scroll Reveal
Words fade from dim to bright as user scrolls — Apple product page style.
// Split into words
const el = document.querySelector('.word-reveal');
const text = el.textContent.trim();
const words = text.split(/\s+/);
el.innerHTML = words.map(w => `<span class="word" style="display:inline-block;opacity:0.15;transition:opacity 0.4s">${w} </span>`).join('');
// Tie word opacity to scroll position
ScrollTrigger.create({
trigger: el,
start: 'top 80%',
end: 'top 20%',
scrub: true,
onUpdate: (self) => {
const wordEls = el.querySelectorAll('.word');
wordEls.forEach((w, i) => {
w.style.opacity = self.progress > (i / wordEls.length) ? '1' : '0.15';
});
}
});
Typewriter Effect (Custom — No External Lib)
Realistic character-by-character typing with cursor and variable speed.
class Typer {
constructor(el, speed = 35) {
this.el = el;
this.speed = speed;
this.queue = [];
this.typing = false;
this.started = false;
}
type(text, cls = '') {
this.queue.push({ t: 'type', text, cls });
return this;
}
pause(ms) {
this.queue.push({ t: 'pause', ms });
return this;
}
out(html, delay = 80) {
this.queue.push({ t: 'out', html, delay });
return this;
}
async start() {
if (this.typing) return;
this.typing = true;
this.started = true;
for (const item of this.queue) {
if (!this.typing) break;
if (item.t === 'type') {
const div = document.createElement('div');
div.className = item.cls || '';
this.el.appendChild(div);
const oldCur = this.el.querySelector('.cursor');
if (oldCur) oldCur.remove();
for (const ch of item.text) {
if (!this.typing) break;
div.textContent += ch;
await this._w(this.speed + (Math.random() - 0.5) * 24);
}
const cur = document.createElement('span');
cur.className = 'cursor';
cur.style.cssText = 'display:inline-block;width:8px;height:1.1em;background:currentColor;animation:blink 1s step-end infinite;vertical-align:text-bottom;margin-left:1px';
div.appendChild(cur);
await this._w(200);
} else if (item.t === 'pause') {
await this._w(item.ms);
} else if (item.t === 'out') {
const oldCur = this.el.querySelector('.cursor');
if (oldCur) oldCur.remove();
await this._w(item.delay);
this.el.insertAdjacentHTML('beforeend', item.html);
}
}
this.typing = false;
}
stop() { this.typing = false; }
reset() { this.stop(); this.el.innerHTML = ''; this.started = false; }
_w(ms) { return new Promise(r => setTimeout(r, ms)); }
}
// Usage:
const typer = new Typer(document.getElementById('terminal'));
typer
.type('$ npm install my-app', 'prompt')
.pause(300)
.out('<div style="color:#A6E3A1;">Done in 2.1s</div>')
.type('$ npm start', 'prompt');
// Trigger on scroll
ScrollTrigger.create({
trigger: '#terminal-section',
start: 'top 60%',
onEnter: () => { if (!typer.started) typer.start(); },
onLeaveBack: () => { typer.reset(); }
});
Number Counter
Animate a number counting up from 0 to target.
// With GSAP (no extra lib)
const numEl = document.querySelector('.stat-number');
const target = parseInt(numEl.dataset.target);
gsap.from(numEl, {
textContent: 0,
duration: 1.5,
snap: { textContent: 1 },
ease: 'power2.out',
scrollTrigger: {
trigger: numEl,
start: 'top 75%',
}
});
// With CountUp.js (more formatting options)
const counter = new countUp.CountUp(numEl, target, {
duration: 2,
separator: ',',
enableScrollSpy: true,
scrollSpyOnce: true,
});
counter.start();
3B. Scroll-Driven Animations
Pinned Section with Scrub Timeline
Pin a section and drive animation with scroll position. Apple product page effect.
gsap.registerPlugin(ScrollTrigger);
const tl = gsap.timeline({
scrollTrigger: {
trigger: '#pinned-section',
start: 'top top',
end: '+=200%', // 2x viewport height of scrolling
pin: true, // Pin the section
scrub: 0.8, // Smooth scrub tied to scroll
}
});
tl.from('.hero-title', { scale: 0.8, opacity: 0, duration: 1 })
.from('.hero-subtitle', { opacity: 0, y: 30, filter: 'blur(10px)', duration: 0.6 }, '-=0.3')
.from('.hero-cta', { opacity: 0, y: 20, duration: 0.5 }, '-=0.2');
ScrollSmoother — Butter-Smooth Scrolling + Parallax
Wraps the page for smooth scrolling. Add data-speed to any element for parallax.
<!-- HTML structure required -->
<div id="smooth-wrapper">
<div id="smooth-content">
<!-- All page content here -->
<h1 data-speed="0.8">Slower heading</h1>
<img data-speed="1.2" src="bg.jpg" alt="">
</div>
</div>
gsap.registerPlugin(ScrollTrigger, ScrollSmoother);
const smoother = ScrollSmoother.create({
wrapper: '#smooth-wrapper',
content: '#smooth-content',
smooth: 1.2, // Smoothing amount (higher = smoother)
effects: true, // Enable data-speed parallax
});
/* Required CSS */
#smooth-wrapper {
overflow: hidden;
position: fixed;
height: 100%;
width: 100%;
top: 0;
left: 0;
}
#smooth-content {
overflow: visible;
width: 100%;
}
Simple Scroll-Triggered Fade-In (No Library)
Minimal approach using Intersection Observer only.
const observer = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) e.target.classList.add('visible');
});
}, { threshold: 0.15 });
document.querySelectorAll('.animate-in').forEach(el => observer.observe(el));
.animate-in {
opacity: 0;
transform: translateY(40px);
transition: opacity 0.8s cubic-bezier(0.16,1,0.3,1),
transform 0.8s cubic-bezier(0.16,1,0.3,1);
}
.animate-in.visible {
opacity: 1;
transform: translateY(0);
}
/* Stagger children */
.animate-in:nth-child(1) { transition-delay: 0.1s; }
.animate-in:nth-child(2) { transition-delay: 0.2s; }
.animate-in:nth-child(3) { transition-delay: 0.3s; }
.animate-in:nth-child(4) { transition-delay: 0.4s; }
Parallax Headings
Headings move at a different rate than content, creating depth.
document.querySelectorAll('section h2').forEach(h => {
gsap.to(h, {
y: -30,
scrollTrigger: {
trigger: h.closest('section'),
start: 'top bottom',
end: 'bottom top',
scrub: 1.5,
}
});
});
3C. 3D / WebGL — Three.js Particle Mesh Background
Atmospheric particle network — nodes connected by faint lines, drifting with scroll-driven color shifts.
(function initParticles() {
const canvas = document.getElementById('bgCanvas');
const renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.z = 20;
const COUNT = 80;
const positions = new Float32Array(COUNT * 3);
const velocities = [];
for (let i = 0; i < COUNT; i++) {
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
const r = 8 + Math.random() * 7;
positions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
positions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
positions[i * 3 + 2] = r * Math.cos(phi);
velocities.push({
x: (Math.random() - 0.5) * 0.006,
y: (Math.random() - 0.5) * 0.006,
z: (Math.random() - 0.5) * 0.006
});
}
const pointGeo = new THREE.BufferGeometry();
pointGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const pointMat = new THREE.PointsMaterial({
color: 0xE07A5F, size: 0.06, transparent: true, opacity: 0.3, sizeAttenuation: true
});
scene.add(new THREE.Points(pointGeo, pointMat));
// Connecting lines
const MAX_LINES = COUNT * COUNT;
const linePos = new Float32Array(MAX_LINES * 6);
const lineGeo = new THREE.BufferGeometry();
lineGeo.setAttribute('position', new THREE.BufferAttribute(linePos, 3));
const lineMat = new THREE.LineBasicMaterial({ color: 0xE07A5F, transparent: true, opacity: 0.06 });
scene.add(new THREE.LineSegments(lineGeo, lineMat));
const THRESHOLD = 3.5;
function animate() {
requestAnimationFrame(animate);
const pos = pointGeo.attributes.position.array;
// Drift particles
for (let i = 0; i < COUNT; i++) {
pos[i*3] += velocities[i].x;
pos[i*3+1] += velocities[i].y;
pos[i*3+2] += velocities[i].z;
if (Math.sqrt(pos[i*3]**2 + pos[i*3+1]**2 + pos[i*3+2]**2) > 16) {
velocities[i].x *= -1; velocities[i].y *= -1; velocities[i].z *= -1;
}
}
pointGeo.attributes.position.needsUpdate = true;
// Update connecting lines
let idx = 0;
const lp = lineGeo.attributes.position.array;
for (let i = 0; i < COUNT; i++) {
for (let j = i+1; j < COUNT; j++) {
const d = Math.sqrt((pos[i*3]-pos[j*3])**2 + (pos[i*3+1]-pos[j*3+1])**2 + (pos[i*3+2]-pos[j*3+2])**2);
if (d < THRESHOLD && idx < MAX_LINES * 6) {
lp[idx++]=pos[i*3]; lp[idx++]=pos[i*3+1]; lp[idx++]=pos[i*3+2];
lp[idx++]=pos[j*3]; lp[idx++]=pos[j*3+1]; lp[idx++]=pos[j*3+2];
}
}
}
for (let k = idx; k < MAX_LINES * 6; k++) lp[k] = 0;
lineGeo.attributes.position.needsUpdate = true;
lineGeo.setDrawRange(0, idx / 3);
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
})();
/* Canvas styling */
#bgCanvas {
position: fixed;
top: 0; left: 0;
width: 100vw; height: 100vh;
z-index: 0;
pointer-events: none;
}
3D. SVG Animations
SVG Path Drawing (GSAP DrawSVGPlugin)
Animate an SVG stroke drawing progressively on scroll.
gsap.registerPlugin(DrawSVGPlugin, ScrollTrigger);
// Draw a path from 0% to 100%
gsap.from('.draw-path', {
drawSVG: '0%',
duration: 2,
ease: 'power2.inOut',
scrollTrigger: {
trigger: '.draw-path',
start: 'top 70%',
toggleActions: 'play none none none',
}
});
SVG Morphing (GSAP MorphSVGPlugin)
Morph one SVG shape into another.
gsap.registerPlugin(MorphSVGPlugin);
gsap.to('#shape1', {
morphSVG: '#shape2',
duration: 1.5,
ease: 'power2.inOut',
scrollTrigger: {
trigger: '#morph-section',
start: 'top 60%',
}
});
3E. Canvas 2D & Particles
Confetti Burst (canvas-confetti)
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9/dist/confetti.browser.min.js"></script>
// Trigger confetti on a milestone
function celebrate() {
confetti({
particleCount: 100,
spread: 70,
origin: { y: 0.6 }
});
}
// Trigger when element enters viewport
ScrollTrigger.create({
trigger: '#milestone',
start: 'top 60%',
onEnter: celebrate,
once: true,
});
3F. Micro-Interactions
Magnetic Button
Button element is attracted toward the cursor when nearby.
document.querySelectorAll('.magnetic').forEach(btn => {
btn.addEventListener('mousemove', (e) => {
const rect = btn.getBoundingClientRect();
const x = e.clientX - rect.left - rect.width / 2;
const y = e.clientY - rect.top - rect.height / 2;
gsap.to(btn, { x: x * 0.3, y: y * 0.3, duration: 0.3, ease: 'power2.out' });
});
btn.addEventListener('mouseleave', () => {
gsap.to(btn, { x: 0, y: 0, duration: 0.5, ease: 'elastic.out(1, 0.3)' });
});
});
3D Card Tilt on Hover
document.querySelectorAll('.tilt-card').forEach(card => {
card.style.transformStyle = 'preserve-3d';
card.style.perspective = '1000px';
card.addEventListener('mousemove', (e) => {
const rect = card.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width - 0.5;
const y = (e.clientY - rect.top) / rect.height - 0.5;
card.style.transform = `rotateY(${x * 10}deg) rotateX(${-y * 10}deg)`;
});
card.addEventListener('mouseleave', () => {
card.style.transform = 'rotateY(0) rotateX(0)';
card.style.transition = 'transform 0.5s ease';
});
card.addEventListener('mouseenter', () => {
card.style.transition = 'none';
});
});
3G. Layout Animations
FLIP Layout Animation (GSAP Flip)
Smoothly animate elements between two layout states.
gsap.registerPlugin(Flip);
const items = document.querySelectorAll('.grid-item');
const state = Flip.getState(items); // Record current positions
// Change layout (e.g., reorder, filter, resize)
container.classList.toggle('list-view');
// Animate from old positions to new
Flip.from(state, {
duration: 0.6,
ease: 'power2.inOut',
stagger: 0.05,
absolute: true,
});
3H. Annotation & Emphasis (Rough Notation)
Hand-drawn highlights, underlines, circles, and boxes that animate onto text.
// Highlight a key phrase
const highlight = RoughNotation.annotate(
document.getElementById('key-phrase'),
{ type: 'highlight', color: 'rgba(245, 158, 11, 0.25)', padding: 4, animationDuration: 1200 }
);
// Underline
const underline = RoughNotation.annotate(
document.getElementById('important-term'),
{ type: 'underline', color: '#E07A5F', strokeWidth: 2, animationDuration: 1000 }
);
// Circle
const circle = RoughNotation.annotate(
document.getElementById('hero-stat'),
{ type: 'circle', color: '#E07A5F', padding: 10, animationDuration: 1500 }
);
// Box
const box = RoughNotation.annotate(
document.getElementById('callout'),
{ type: 'box', color: '#3B82F6', padding: 6, animationDuration: 800 }
);
// Trigger on scroll
ScrollTrigger.create({
trigger: '#annotation-section',
start: 'top 50%',
onEnter: () => {
highlight.show();
setTimeout(() => underline.show(), 500);
setTimeout(() => circle.show(), 1000);
},
once: true,
});
// Group annotations for sequential reveal
const group = RoughNotation.annotationGroup([highlight, underline, circle, box]);
group.show(); // Plays one after another automatically
Phase 4: Performance & Accessibility
GPU vs CPU — What to Animate
GPU-composited (FAST):
transform: translate(),scale(),rotate()opacityfilter: blur(),brightness()
CPU-bound (SLOW — avoid animating these):
width,height,top,left,margin,paddingborder-width,border-radiusbox-shadow,background-color,color
Performance Tier List
| Tier | Method |
|---|---|
| S | CSS transitions on transform/opacity |
| S | Web Animations API (WAAPI) — used by Motion.dev |
| A | CSS Scroll-Driven Animations (native, off-main-thread) |
| A | GSAP on transform/opacity |
| B | GSAP/anime.js on paint-only properties (color, bg) |
| C | JS on layout properties (width, height, top, left) |
| D | jQuery .animate() |
Reduced Motion — CRITICAL
Always respect prefers-reduced-motion:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
if (reducedMotion.matches) {
gsap.globalTimeline.timeScale(100);
}
Canvas/WebGL Tips
- Cap pixel ratio:
Math.min(window.devicePixelRatio, 2) - Use a single
requestAnimationFrameloop for all canvas work - Use
InstancedMeshin Three.js for repeated objects - Object pool particles instead of create/destroy
- Consider
OffscreenCanvasfor worker-thread rendering
Quick Reference: Easing Guide
Decision Flowchart
Scroll-linked / progress? --> LINEAR
Element ENTERING view? --> power2.out or power3.out
Element LEAVING view? --> power2.in
State change in view? --> power2.inOut or SPRING
Interactive (drag/click)? --> SPRING
Ambient / looping? --> sine.inOut
Default fallback? --> power2.out
Power Scale (GSAP)
| Easing | Character | Use Case |
|---|---|---|
power1.out |
Subtle | Corporate, conservative UIs |
power2.out |
Sweet spot | Most UI animations |
power3.out |
Dramatic | Hero sections, impactful reveals |
expo.out |
Extreme | Menus, modals, page transitions |
Specialty Easings
| Easing | GSAP | Use Case |
|---|---|---|
| Bounce | bounce.out |
Playful UI, game-like |
| Elastic | elastic.out(1, 0.3) |
Creative sites, notifications |
| Back | back.out(1.7) |
Button clicks, modal entrances |
| Sine | sine.inOut |
Breathing, ambient loops |
Quick Reference: Audience Mapping
| Audience | Animations to Use | Avoid | Easing | Duration |
|---|---|---|---|---|
| Corporate | Fade-in, counters, Rough Notation, smooth scroll | Particles, 3D, elastic, confetti | power2.out |
0.4-0.8s |
| Developer | Typewriter, code scroll, SVG drawing, terminal mockups | Organic effects, heavy 3D | power3.out |
0.3-0.6s |
| Creative | 3D particles, SVG morphing, cursor trails, parallax, SplitText | Generic fades, template looks | elastic, back, springs |
0.6-1.5s |
| Marketing | Hero text reveals, counters, confetti, Rough Notation, parallax | Slow animations, dense data viz | power2.out, back |
0.4-0.8s |
| Educational | Scroll-driven reveals, SVG drawing, annotations, typewriter, pinned sections | Fast animations, particle effects | power2.inOut, sine |
0.5-1.0s |
Troubleshooting
Common Issues
SplitText makes gradient text invisible:
-webkit-text-fill-color: transparentdoesn't propagate to split char elements- Fix: Use solid colors on split text, or apply gradient to each char via CSS
ScrollTrigger pinning causes layout shift:
- Ensure pinned section has no margin that could cause offset
- Use
pinSpacing: true(default) or set explicit spacer height
Three.js canvas covers interactive elements:
- Set
pointer-events: noneon the canvas - Set
z-index: 0on canvas,z-index: 1on content
Animations not triggering:
- Check that GSAP plugins are registered:
gsap.registerPlugin(ScrollTrigger, SplitText, ...) - Verify elements exist in DOM before animation code runs
- For ScrollSmoother: content must be inside
#smooth-wrapper > #smooth-content
Performance issues (jank):
- Only animate
transformandopacity - Reduce particle count on mobile
- Use
will-changesparingly, remove after animation - Throttle scroll/mousemove handlers
Fonts not loaded when SplitText runs:
- SplitText measures character widths; if fonts haven't loaded, measurements are wrong
- Fix: Wait for fonts:
document.fonts.ready.then(() => { /* init SplitText */ })
Related Skills
- frontend-slides -- For building complete HTML slide presentations with animations
- frontend-design -- For more complex interactive pages and component design
Example Session Flow
- User: "Add scroll animations to my landing page"
- Skill asks: Audience? Scope? Existing libraries?
- User: "Marketing page, text + scroll effects, no libraries yet"
- Skill recommends: Minimal Stack (GSAP + ScrollTrigger + SplitText + Rough Notation)
- Skill reads the HTML file, identifies headings and sections
- Skill adds: SplitText reveals on h1/h2, scroll-triggered fades on cards, Rough Notation on key stats, number counters on metrics
- Skill opens page in browser
- User: "Can you add a particle background too?"
- Skill adds Three.js particle mesh from the 3C recipe
- Final page delivered with smooth, performant animations