Composables

useGsap()

Access the GSAP instance or create a scoped animation context with automatic cleanup.

useGsap() is auto-imported in every component and composable. It supports two call signatures.

Which form should I use?

  • Zero-argument — Quick prototyping or when you need full manual control over cleanup. You manage timelines, contexts, and plugin instances yourself.
  • Context form (recommended) — For most components. Automatic cleanup, SSR-safe, page-transition aware, and scoped to your element. No boilerplate.

Zero-argument form

Returns the raw gsap instance. Use this when you want full control and will manage cleanup manually.

components/AnimatedBox.vue
<script setup lang="ts">
const gsap = useGsap()
const boxRef = ref<HTMLElement | null>(null)

onMounted(() => {
  gsap.from(boxRef.value, { opacity: 0, scale: 0.8, duration: 0.5 })
})
</script>

<template>
  <div ref="boxRef" class="box">Hello!</div>
</template>

You can also use the auto-imported gsap global directly — both are equivalent.

Timeline example:

components/TimelineBox.vue
<script setup lang="ts">
const gsap = useGsap()
const boxRef = ref<HTMLElement | null>(null)

onMounted(() => {
  const tl = gsap.timeline({ repeat: -1, yoyo: true })
  tl.to(boxRef.value, { x: 200, duration: 1, ease: 'power2.inOut' })
    .to(boxRef.value, { rotation: 360, duration: 0.8 })
})
</script>

<template>
  <div ref="boxRef" class="box" />
</template>

Pass a setup function to wrap your animations in a gsap.context(). The context is automatically reverted when the component unmounts — for pages with a transition, the revert is deferred to Nuxt's page:transition:finish hook so it runs after the fade-out. No cleanup boilerplate needed.

Advantages

  1. Auto-cleanup — All tweens, timelines, and plugin instances (ScrollTrigger, Draggable, etc.) are reverted automatically without a single onUnmounted.
  2. SSR-safe — Returns { contextSafe: fn => fn } on the server, so no DOM access during SSR builds.
  3. Page-transition aware — when the leaving page has a transition, the revert is explicitly deferred to Nuxt's page:transition:finish hook, so exit animations play smoothly through the fade-out.
  4. Scoped selectors by default — CSS class/id selectors are scoped to the current component's root element (or the scope you pass), preventing selector leaks to same-class elements in other components. Pass scope: null to match globally.
  5. Reactive re-runs — Pass dependencies to re-run the setup function when reactive values change, with automatic context revert + re-run after the DOM update.

Before and after

Without context form (manual cleanup):

const gsap = useGsap()
let ctx: gsap.Context

onMounted(() => {
  ctx = gsap.context(() => {
    gsap.from('.box', { opacity: 0, y: 30 })
    ScrollTrigger.create({ trigger: '.box', start: 'top 80%' })
  })
})

onUnmounted(() => ctx.revert()) // must remember this

With context form (recommended):

useGsap(() => {
  gsap.from('.box', { opacity: 0, y: 30 })
  ScrollTrigger.create({ trigger: '.box', start: 'top 80%' })
  // auto-reverted — ScrollTrigger too
}, { scope: containerRef })
components/ContextBox.vue
<script setup lang="ts">
const containerRef = ref<HTMLElement | null>(null)

useGsap(() => {
  gsap.from('.box', { opacity: 0, y: 30, duration: 0.6, ease: 'power2.out' })
}, { scope: containerRef })
</script>

<template>
  <div ref="containerRef">
    <div class="box">I animate in safely</div>
  </div>
</template>

Options

OptionTypeDefaultDescription
scopeElement | Ref | component ref | nullcomponent root elementScopes class/id selectors inside the element. Accepts a plain element, template ref, or component ref ($el is used). null opts out (global selectors).
dependenciesWatchSource[][]Re-run setup when any source changes
revertOnUpdatebooleantrueRevert the previous context before re-running
cleanupOn'unmount' | 'route-leave''unmount'Deprecated, no-op. Cleanup is always transition-aware (see note below). Kept for backward compatibility.
Behavior changeuseGsap(setup) now scopes selectors to the current component's root element by default (previously selectors matched globally when no scope was passed). If you relied on global selectors, pass scope: null explicitly.
Cleanup timingWhen the leaving page has a transition (per-page definePageMeta({ pageTransition }) or a global app.pageTransition), useGsap(setup) defers the context revert to Nuxt's page:transition:finish hook, so animations keep playing through the fade-out. Without a transition, the revert happens immediately in onUnmounted. Vue itself flushes unmounted hooks at leave start (see vuejs/core#994), which is why the module defers explicitly instead of relying on unmount timing. The deprecated cleanupOn option is accepted for compatibility, but it does not change runtime behavior.

contextSafe

The setup callback receives a self reference to the GSAP context. contextSafe is returned so you can wrap event handlers that create animations after mount:

const { contextSafe } = useGsap((self) => {
  // initial animations
}, { scope: containerRef })

const handleClick = contextSafe(() => {
  gsap.to('.box', { rotation: '+=90' })
})

With reactive dependencies

const color = ref('red')

useGsap(() => {
  gsap.to('.box', { backgroundColor: color.value, duration: 0.3 })
}, { dependencies: [color] })

Every time color changes the context reverts and re-runs after Vue flushes the DOM update cycle (controlled by revertOnUpdate).

Using with plugin composables

You can call plugin composables inside the setup function to access static methods (getAll(), refresh(), create()). The key is: call the composable outside useGsap to get the instance, but run animations (and their cleanup) inside the setup function.

With ScrollTrigger

<script setup lang="ts">
const ScrollTrigger = useScrollTrigger()
const containerRef = ref<HTMLElement | null>(null)

useGsap(() => {
  gsap.from('.card', {
    opacity: 0,
    y: 40,
    stagger: 0.1,
    scrollTrigger: {
      trigger: '.card',
      start: 'top 85%',
      end: 'top 50%',
      scrub: true,
    },
  })
  // ScrollTrigger.refresh() available via the composable outside
}, { scope: containerRef })
</script>

<template>
  <div ref="containerRef">
    <div class="card">Card 1</div>
    <div class="card">Card 2</div>
  </div>
</template>
Call the plugin composable outside useGsap — you only need the instance for static methods like refresh() or getAll(). The animations and ScrollTriggers created inside the setup function are what gets auto-reverted when the component unmounts.

With Draggable and contextSafe

Event handlers that add animations after mount should be wrapped with contextSafe:

const Draggable = useDraggable()
const { contextSafe } = useGsap(() => {
  Draggable.create(boxRef.value, { type: 'x,y' })
})

const handleDragEnd = contextSafe(() => {
  gsap.to(boxRef.value, { scale: 1.1, duration: 0.2 })
})

All animations and Draggable instances created inside contextSafe are part of the same context and get auto-reverted.

Zero-arg form: cleanup guide

Use these patterns only when using the zero-argument form of useGsap() and you need manual control. For most components, prefer the context form above.

gsap.context() — covers all child tweens, timelines, and ScrollTriggers:

<script setup lang="ts">
const gsap = useGsap()
let ctx: gsap.Context

onMounted(() => {
  ctx = gsap.context(() => {
    gsap.to('.box', { x: 100 })
  })
})

onUnmounted(() => ctx.revert())
</script>

Single timeline:

const gsap = useGsap()
const tl = gsap.timeline()
onUnmounted(() => tl.kill())

Plugin instance (e.g. Draggable):

const Draggable = useDraggable()
let draggables: ReturnType<typeof Draggable.create> = []

onMounted(() => {
  draggables = Draggable.create(boxRef.value)
})

onUnmounted(() => {
  draggables.forEach(d => d.kill())
  draggables = []
})
Plugin registration is handled once by the module at app startup. In components, only kill the instances you created (tweens, timelines, Draggables, ScrollTriggers). Do not call gsap.registerPlugin() manually.
Copyright © 2026