gsap

Smooth, GPU-accelerated animations for JARVIS HUD panels, transitions, and data visualizations. Covers panel entrance/exit, status indicators, data visualization, and scroll-triggered effects with TDD-first implementation workflow Enforces performance best practices: GPU-accelerated transforms only, will-change property management, and strict animation cleanup on unmount Includes accessibility support for reduced motion preferences and memory leak prevention through proper timeline and tween lifecycle management Provides composable patterns for reusable animations, timeline sequences, and ScrollTrigger batching with lazy initialization strategies

INSTALLATION
npx skills add https://github.com/martinholovsky/claude-skills-generator --skill gsap
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

GSAP Animation Skill

File Organization: This skill uses split structure. See references/ for advanced patterns.

1. Overview

This skill provides GSAP (GreenSock Animation Platform) expertise for creating smooth, professional animations in the JARVIS AI Assistant HUD.

Risk Level: LOW - Animation library with minimal security surface

Primary Use Cases:

  • HUD panel entrance/exit animations
  • Status indicator transitions
  • Data visualization animations
  • Scroll-triggered effects
  • Complex timeline sequences

2. Core Responsibilities

2.1 Fundamental Principles

  • TDD First: Write animation tests before implementation
  • Performance Aware: Use transforms/opacity for GPU acceleration, avoid layout thrashing
  • Cleanup Required: Always kill animations on component unmount
  • Timeline Organization: Use timelines for complex sequences
  • Easing Selection: Choose appropriate easing for HUD feel
  • Accessibility: Respect reduced motion preferences
  • Memory Management: Avoid memory leaks with proper cleanup

2.5 Implementation Workflow (TDD)

Step 1: Write Failing Test First

// tests/animations/panel-animation.test.ts

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'

import { mount } from '@vue/test-utils'

import { gsap } from 'gsap'

import HUDPanel from '~/components/HUDPanel.vue'

describe('HUDPanel Animation', () => {

  beforeEach(() => {

    // Mock reduced motion

    Object.defineProperty(window, 'matchMedia', {

      writable: true,

      value: vi.fn().mockImplementation(query => ({

        matches: false,

        media: query

      }))

    })

  })

  afterEach(() => {

    // Verify cleanup

    gsap.globalTimeline.clear()

  })

  it('animates panel entrance with correct properties', async () => {

    const wrapper = mount(HUDPanel)

    // Wait for animation to complete

    await new Promise(resolve => setTimeout(resolve, 600))

    const panel = wrapper.find('.hud-panel')

    expect(panel.exists()).toBe(true)

  })

  it('cleans up animations on unmount', async () => {

    const wrapper = mount(HUDPanel)

    const childCount = gsap.globalTimeline.getChildren().length

    await wrapper.unmount()

    // All animations should be killed

    expect(gsap.globalTimeline.getChildren().length).toBeLessThan(childCount)

  })

  it('respects reduced motion preference', async () => {

    // Mock reduced motion enabled

    window.matchMedia = vi.fn().mockImplementation(() => ({

      matches: true

    }))

    const wrapper = mount(HUDPanel)

    const panel = wrapper.find('.hud-panel').element

    // Should set final state immediately without animation

    expect(gsap.getProperty(panel, 'opacity')).toBe(1)

  })

})

Step 2: Implement Minimum to Pass

// components/HUDPanel.vue - implement animation logic

const animation = ref<gsap.core.Tween | null>(null)

onMounted(() => {

  if (!panelRef.value) return

  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {

    gsap.set(panelRef.value, { opacity: 1 })

    return

  }

  animation.value = gsap.from(panelRef.value, {

    opacity: 0,

    y: 20,

    duration: 0.5

  })

})

onUnmounted(() => {

  animation.value?.kill()

})

Step 3: Refactor Following Patterns

// Extract to composable for reusability

export function usePanelAnimation(elementRef: Ref<HTMLElement | null>) {

  const animation = ref<gsap.core.Tween | null>(null)

  const animate = () => {

    if (!elementRef.value) return

    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {

      gsap.set(elementRef.value, { opacity: 1 })

      return

    }

    animation.value = gsap.from(elementRef.value, {

      opacity: 0,

      y: 20,

      duration: 0.5,

      ease: 'power2.out'

    })

  }

  onMounted(animate)

  onUnmounted(() => animation.value?.kill())

  return { animation }

}

Step 4: Run Full Verification

# Run animation tests

npm test -- --grep "Animation"

# Check for memory leaks

npm run test:memory

# Verify 60fps performance

npm run test:performance

3. Technology Stack &#x26; Versions

3.1 Recommended Versions

Package

Version

Notes

gsap

^3.12.0

Core library

@gsap/vue

^3.12.0

Vue integration

ScrollTrigger

included

Scroll effects

3.2 Vue Integration

// plugins/gsap.ts

import { gsap } from 'gsap'

import { ScrollTrigger } from 'gsap/ScrollTrigger'

export default defineNuxtPlugin(() => {

  gsap.registerPlugin(ScrollTrigger)

  return {

    provide: {

      gsap,

      ScrollTrigger

    }

  }

})

4. Implementation Patterns

4.1 Panel Entrance Animation

<script setup lang="ts">

import { gsap } from 'gsap'

import { onMounted, onUnmounted, ref } from 'vue'

const panelRef = ref<HTMLElement | null>(null)

let animation: gsap.core.Tween | null = null

onMounted(() => {

  if (!panelRef.value) return

  // ✅ Check reduced motion preference

  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {

    gsap.set(panelRef.value, { opacity: 1 })

    return

  }

  animation = gsap.from(panelRef.value, {

    opacity: 0,

    y: 20,

    scale: 0.95,

    duration: 0.5,

    ease: 'power2.out'

  })

})

// ✅ Cleanup on unmount

onUnmounted(() => {

  animation?.kill()

})

</script>

<template>

  <div ref="panelRef" class="hud-panel">

    <slot />

  </div>

</template>

4.2 Status Indicator Animation

// composables/useStatusAnimation.ts

import { gsap } from 'gsap'

export function useStatusAnimation(element: Ref<HTMLElement | null>) {

  const timeline = ref<gsap.core.Timeline | null>(null)

  const animateStatus = (status: string) => {

    if (!element.value) return

    timeline.value?.kill()

    timeline.value = gsap.timeline()

    switch (status) {

      case 'active':

        timeline.value

          .to(element.value, {

            scale: 1.2,

            duration: 0.2,

            ease: 'power2.out'

          })

          .to(element.value, {

            scale: 1,

            duration: 0.3,

            ease: 'elastic.out(1, 0.3)'

          })

        break

      case 'warning':

        timeline.value.to(element.value, {

          backgroundColor: '#f59e0b',

          boxShadow: '0 0 10px #f59e0b',

          duration: 0.3,

          repeat: 2,

          yoyo: true

        })

        break

      case 'error':

        timeline.value.to(element.value, {

          x: -5,

          duration: 0.05,

          repeat: 5,

          yoyo: true

        })

        break

    }

  }

  onUnmounted(() => {

    timeline.value?.kill()

  })

  return { animateStatus }

}

4.3 Data Visualization Animation

<script setup lang="ts">

import { gsap } from 'gsap'

const props = defineProps<{

  data: number[]

}>()

const barsRef = ref<HTMLElement[]>([])

let animations: gsap.core.Tween[] = []

watch(() => props.data, (newData) => {

  // Kill previous animations

  animations.forEach(a => a.kill())

  animations = []

  // Animate each bar

  newData.forEach((value, index) => {

    const bar = barsRef.value[index]

    if (!bar) return

    const tween = gsap.to(bar, {

      height: `${value}%`,

      duration: 0.5,

      delay: index * 0.05,

      ease: 'power2.out'

    })

    animations.push(tween)

  })

}, { immediate: true })

onUnmounted(() => {

  animations.forEach(a => a.kill())

})

</script>

<template>

  <div class="flex items-end h-40 gap-1">

    <div

      v-for="(_, index) in data"

      :key="index"

      ref="barsRef"

      class="w-4 bg-jarvis-primary"

    />

  </div>

</template>

4.4 Timeline Sequence

// Create complex HUD startup sequence

export function createStartupSequence(elements: {

  logo: HTMLElement

  panels: HTMLElement[]

  status: HTMLElement

}): gsap.core.Timeline {

  const tl = gsap.timeline({

    defaults: { ease: 'power2.out' }

  })

  // Logo reveal

  tl.from(elements.logo, {

    opacity: 0,

    scale: 0,

    duration: 0.8,

    ease: 'back.out(1.7)'

  })

  // Panels stagger in

  tl.from(elements.panels, {

    opacity: 0,

    x: -30,

    stagger: 0.1,

    duration: 0.5

  }, '-=0.3')

  // Status indicator

  tl.from(elements.status, {

    opacity: 0,

    y: 10,

    duration: 0.3

  }, '-=0.2')

  return tl

}

4.5 Scroll-Triggered Animation

<script setup lang="ts">

import { gsap } from 'gsap'

import { ScrollTrigger } from 'gsap/ScrollTrigger'

const sectionRef = ref<HTMLElement | null>(null)

onMounted(() => {

  if (!sectionRef.value) return

  gsap.from(sectionRef.value.querySelectorAll('.animate-item'), {

    scrollTrigger: {

      trigger: sectionRef.value,

      start: 'top 80%',

      end: 'bottom 20%',

      toggleActions: 'play none none reverse'

    },

    opacity: 0,

    y: 30,

    stagger: 0.1,

    duration: 0.5

  })

})

onUnmounted(() => {

  ScrollTrigger.getAll().forEach(trigger => trigger.kill())

})

</script>

5. Quality Standards

5.1 Performance

// ✅ GOOD - Use transforms for GPU acceleration

gsap.to(element, {

  x: 100,

  y: 50,

  rotation: 45,

  scale: 1.2

})

// ❌ BAD - Triggers layout recalculation

gsap.to(element, {

  left: 100,

  top: 50,

  width: '120%'

})

5.2 Accessibility

// ✅ Respect reduced motion

const prefersReducedMotion = window.matchMedia(

  '(prefers-reduced-motion: reduce)'

).matches

if (prefersReducedMotion) {

  gsap.set(element, { opacity: 1 })

} else {

  gsap.from(element, { opacity: 0, duration: 0.5 })

}

6. Performance Patterns

6.1 will-change Property Usage

// Good: Apply will-change before animation

const animatePanel = (element: HTMLElement) => {

  element.style.willChange = 'transform, opacity'

  gsap.to(element, {

    x: 100,

    opacity: 0.8,

    duration: 0.5,

    onComplete: () => {

      element.style.willChange = 'auto'

    }

  })

}

// Bad: Never removing will-change

const animatePanelBad = (element: HTMLElement) => {

  element.style.willChange = 'transform, opacity' // Memory leak!

  gsap.to(element, { x: 100, opacity: 0.8 })

}

6.2 Transform vs Layout Properties

// Good: Use transforms (GPU accelerated)

gsap.to(element, {

  x: 100,           // translateX

  y: 50,            // translateY

  scale: 1.2,       // scale

  rotation: 45,     // rotate

  opacity: 0.5      // opacity

})

// Bad: Layout-triggering properties (CPU, causes reflow)

gsap.to(element, {

  left: 100,        // Triggers layout

  top: 50,          // Triggers layout

  width: '120%',    // Triggers layout

  height: 200,      // Triggers layout

  margin: 10        // Triggers layout

})

6.3 Timeline Reuse

// Good: Reuse timeline instance

const timeline = gsap.timeline({ paused: true })

timeline

  .to(element, { opacity: 1, duration: 0.3 })

  .to(element, { y: -20, duration: 0.5 })

// Play/reverse as needed

const show = () => timeline.play()

const hide = () => timeline.reverse()

// Bad: Creating new timeline each time

const showBad = () => {

  gsap.timeline()

    .to(element, { opacity: 1, duration: 0.3 })

    .to(element, { y: -20, duration: 0.5 })

}

6.4 ScrollTrigger Batching

// Good: Batch ScrollTrigger animations

ScrollTrigger.batch('.animate-item', {

  onEnter: (elements) => {

    gsap.to(elements, {

      opacity: 1,

      y: 0,

      stagger: 0.1,

      overwrite: true

    })

  },

  onLeave: (elements) => {

    gsap.to(elements, {

      opacity: 0,

      y: -20,

      overwrite: true

    })

  }

})

// Bad: Individual ScrollTrigger per element

document.querySelectorAll('.animate-item').forEach(item => {

  gsap.to(item, {

    scrollTrigger: {

      trigger: item,

      start: 'top 80%'

    },

    opacity: 1,

    y: 0

  })

})

6.5 Lazy Initialization

// Good: Initialize animations only when needed

let panelAnimation: gsap.core.Timeline | null = null

const getPanelAnimation = () => {

  if (!panelAnimation) {

    panelAnimation = gsap.timeline({ paused: true })

      .from('.panel', { opacity: 0, y: 20 })

      .from('.panel-content', { opacity: 0, stagger: 0.1 })

  }

  return panelAnimation

}

const showPanel = () => getPanelAnimation().play()

const hidePanel = () => getPanelAnimation().reverse()

// Bad: Initialize all animations on mount

onMounted(() => {

  // Creates timeline even if never used

  const animation1 = gsap.timeline().to('.panel1', { x: 100 })

  const animation2 = gsap.timeline().to('.panel2', { y: 100 })

  const animation3 = gsap.timeline().to('.panel3', { scale: 1.2 })

})

7. Testing &#x26; Quality

7.1 Animation Testing

describe('Panel Animation', () => {

  it('cleans up on unmount', async () => {

    const wrapper = mount(HUDPanel)

    await wrapper.unmount()

    // No active GSAP animations should remain

    expect(gsap.globalTimeline.getChildren().length).toBe(0)

  })

})

8. Common Mistakes &#x26; Anti-Patterns

8.1 Critical Anti-Patterns

#### Never: Skip Cleanup

// ❌ MEMORY LEAK

onMounted(() => {

  gsap.to(element, { x: 100, duration: 1 })

})

// ✅ PROPER CLEANUP

let tween: gsap.core.Tween

onMounted(() => {

  tween = gsap.to(element, { x: 100, duration: 1 })

})

onUnmounted(() => {

  tween?.kill()

})

#### Never: Animate Layout Properties

// ❌ BAD - Causes layout thrashing

gsap.to(element, { width: 200, height: 100 })

// ✅ GOOD - Use transforms

gsap.to(element, { scaleX: 2, scaleY: 1 })

13. Pre-Implementation Checklist

Phase 1: Before Writing Code

  • Write failing tests for animation behavior
  • Define animation timing and easing requirements
  • Identify elements that need will-change hints
  • Plan cleanup strategy for all animations
  • Check if reduced motion support is needed

Phase 2: During Implementation

  • Use transforms/opacity only (no layout properties)
  • Store animation references for cleanup
  • Apply will-change before, remove after animation
  • Use timelines for sequences
  • Batch ScrollTrigger animations
  • Implement lazy initialization for complex animations

Phase 3: Before Committing

  • All tests pass (npm test -- --grep "Animation")
  • All animations cleaned up on unmount
  • Reduced motion preference respected
  • No memory leaks (check with DevTools)
  • 60fps maintained (test with performance monitor)
  • ScrollTrigger instances properly killed

14. Summary

GSAP provides professional animations for JARVIS HUD:

  • Cleanup: Always kill animations on unmount
  • Performance: Use transforms and opacity only
  • Accessibility: Respect reduced motion preference
  • Organization: Use timelines for sequences

Remember: Every animation must be cleaned up to prevent memory leaks.

References:

  • references/advanced-patterns.md - Complex animation patterns
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card