js-animation

Add stunning JavaScript animations to any web project. Covers text animations, scroll-driven effects, 3D/WebGL, SVG drawing, particles, micro-interactions, and more. Helps choose the right animation approach based on target audience (corporate, developer, creative, marketing, educational), selects optimal libraries (GSAP, Three.js, anime.js, Motion.dev), and provides production-ready code recipes. Use when the user wants to animate a webpage, add scroll effects, create 3D backgrounds, or enhance any HTML with motion.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "js-animation" with this command: npx skills add somsamantray/js-animation-webpage/somsamantray-js-animation-webpage-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

  1. 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.
  2. Audience-Aware — A corporate executive deck needs different motion than a creative portfolio. Always match animation intensity to the audience.
  3. Performance First — Animate only GPU-composited properties (transform, opacity). Respect prefers-reduced-motion. Cap devicePixelRatio. Use a single RAF loop.
  4. CDN-Loadable — All libraries must be loadable via <script> tags from CDNs. No npm, no build tools, no bundlers required.
  5. 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()
  • opacity
  • filter: blur(), brightness()

CPU-bound (SLOW — avoid animating these):

  • width, height, top, left, margin, padding
  • border-width, border-radius
  • box-shadow, background-color, color

Performance Tier List

TierMethod
SCSS transitions on transform/opacity
SWeb Animations API (WAAPI) — used by Motion.dev
ACSS Scroll-Driven Animations (native, off-main-thread)
AGSAP on transform/opacity
BGSAP/anime.js on paint-only properties (color, bg)
CJS on layout properties (width, height, top, left)
DjQuery .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 requestAnimationFrame loop for all canvas work
  • Use InstancedMesh in Three.js for repeated objects
  • Object pool particles instead of create/destroy
  • Consider OffscreenCanvas for 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)

EasingCharacterUse Case
power1.outSubtleCorporate, conservative UIs
power2.outSweet spotMost UI animations
power3.outDramaticHero sections, impactful reveals
expo.outExtremeMenus, modals, page transitions

Specialty Easings

EasingGSAPUse Case
Bouncebounce.outPlayful UI, game-like
Elasticelastic.out(1, 0.3)Creative sites, notifications
Backback.out(1.7)Button clicks, modal entrances
Sinesine.inOutBreathing, ambient loops

Quick Reference: Audience Mapping

AudienceAnimations to UseAvoidEasingDuration
CorporateFade-in, counters, Rough Notation, smooth scrollParticles, 3D, elastic, confettipower2.out0.4-0.8s
DeveloperTypewriter, code scroll, SVG drawing, terminal mockupsOrganic effects, heavy 3Dpower3.out0.3-0.6s
Creative3D particles, SVG morphing, cursor trails, parallax, SplitTextGeneric fades, template lookselastic, back, springs0.6-1.5s
MarketingHero text reveals, counters, confetti, Rough Notation, parallaxSlow animations, dense data vizpower2.out, back0.4-0.8s
EducationalScroll-driven reveals, SVG drawing, annotations, typewriter, pinned sectionsFast animations, particle effectspower2.inOut, sine0.5-1.0s

Troubleshooting

Common Issues

SplitText makes gradient text invisible:

  • -webkit-text-fill-color: transparent doesn'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: none on the canvas
  • Set z-index: 0 on canvas, z-index: 1 on 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 transform and opacity
  • Reduce particle count on mobile
  • Use will-change sparingly, 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

  1. User: "Add scroll animations to my landing page"
  2. Skill asks: Audience? Scope? Existing libraries?
  3. User: "Marketing page, text + scroll effects, no libraries yet"
  4. Skill recommends: Minimal Stack (GSAP + ScrollTrigger + SplitText + Rough Notation)
  5. Skill reads the HTML file, identifies headings and sections
  6. Skill adds: SplitText reveals on h1/h2, scroll-triggered fades on cards, Rough Notation on key stats, number counters on metrics
  7. Skill opens page in browser
  8. User: "Can you add a particle background too?"
  9. Skill adds Three.js particle mesh from the 3C recipe
  10. Final page delivered with smooth, performant animations

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

OPC Landing Page Manager

Landing page strategy, copywriting, design, and code generation for solo entrepreneurs. From product idea to a complete, self-contained, conversion-optimized...

Registry SourceRecently Updated
Coding

OPC Product Manager

Product spec generation for solo entrepreneurs. Turns a one-sentence idea into a build-ready spec that AI coding agents (Claude Code, etc.) can execute direc...

Registry SourceRecently Updated
Coding

设备

Use when querying or modifying device configurations on ESD service, calling REST APIs with sigV2 authentication on HK baseline or STG environments

Registry SourceRecently Updated
Coding

My Agent Browser

A fast Rust-based headless browser automation CLI with Node.js fallback that enables AI agents to navigate, click, type, and snapshot pages via structured co...

Registry SourceRecently Updated