How to setup GSAP in Nuxt 3 when references are all over the place
• Diego Hernández Herrera
Today I just lost about 2 hours animating some ScrollTriggers that should have taken about 5 minutes. And even after reading a couple of online articles and consulting the wisdow of multiple LLMs, I just couldn’t get my Vue references to work adequately as GSAP targets.
It was funny, because all my animated components worked like a charm upon first render. But if I navigated to another route and then returned, something in between the navigations broke down my animations. And it seemed like the references serving as GSAP targets simply wouldn’t be ready on time, even when using the onMounted and nextTick functions, and would cause GSAP target null not found errors.
After trial and error, I found a setup that simply works. And even though I’m not entirely sure why this configuration solved my reference errors and not any of the suggestions that ChatGPT or Claude made, I figured I could share it with you so that you can save a couple of minutes.
So the first thing to do is simple: install GSAP.
pnpm install gsap
Now, we’ll create a ./plugins/gsap.client.ts file, where we’ll make sure to provide globally the GSAP object and any other plugin we want.
import { defineNuxtPlugin } from "#app";
import { gsap, ScrollTrigger, SplitText, ScrollSmoother } from "gsap/all";
export default defineNuxtPlugin(() => {
gsap.registerPlugin(ScrollTrigger, SplitText, ScrollSmoother);
return {
provide: {
gsap,
ScrollTrigger,
SplitText,
ScrollSmoother
}
}
});
After setting up this file, we should be able to access these objects all throughout the project with the following line:
const { $gsap, $ScrollTrigger, $SplitText, $ScrollSmoother } = useNuxtApp();
Now, for purposes I’ll explain briefly, we’ll create the following functions on path ./composables/animations.ts:
export const useGsap = () => {
const nuxtApp = useNuxtApp();
return {
gsap: nuxtApp.$gsap,
ScrollTrigger: nuxtApp.$ScrollTrigger,
SplitText: nuxtApp.$SplitText,
ScrollSmoother: nuxtApp.$ScrollSmoother
}
}
export const waitForRefs = (...refs: Array<Ref<HTMLElement | null>>) => {
const refsAreReady = ref(false);
onMounted(async () => {
await nextTick();
const checkReferences = () => refs.every(ref => ref.value !== null);
while (!checkReferences()) {
await nextTick();
}
refsAreReady.value = true;
})
return { refsAreReady };
}
The useGsap function is pretty straight-forward. It simply makes access to our GSAP plugin more elegant. But the waitForRefs function was key to fix all my reference errors. It is something I couldn’t find in any online article and no LLM could provide. It simply cycles through all DOM flushes that are necessary until all references are not null. The way to use it is like this:
<script setup lang="ts">
const props = defineProps<{
title: string;
image: string;
}>()
const { gsap, SplitText } = useGsap()
let ctx: gsap.Context;
const sectionRef = ref<HTMLElement | null>(null)
const titleRef = ref<HTMLElement | null>(null)
const leftSideRef = ref<HTMLElement | null>(null)
const rightSideRef = ref<HTMLElement | null>(null)
const { refsAreReady } = waitForRefs(
sectionRef,
titleRef,
leftSideRef,
rightSideRef
)
const animate = () => {
ctx = gsap.context(() => {
const commonGsapConfig = {
scrollTrigger: {
trigger: sectionRef.value,
start: 'top 40%',
end: 'center center',
scrub: true,
}
}
gsap.from(
leftSideRef.value?.querySelectorAll('p') || [],
{
opacity: 0,
y: 100,
ease: 'power1.out',
stagger: 0.2,
...commonGsapConfig
}
)
SplitText.create(
titleRef.value,
{
type: "words, chars",
onSplit(self) {
gsap.from(
self.chars,
{
opacity: 0,
// x: 100,
y: '2rem',
ease: 'power1.out',
stagger: 0.05,
...commonGsapConfig
}
)
}
}
)
})
}
watchEffect(() => {
if (refsAreReady.value) {
animate()
}
})
onUnmounted(() => {
ctx.revert()
})
</script>
You basically call waitForRefs with the references that you are going to use as GSAP targets. And then you watchEffect the refsAreReady value so that when it finally returns true you can run your gsap animations without a single GSAP target null not found issue.
Don’t forget to setup your animations inside a GSAP context, so that you can cleanup during the unmounting process. Also, the references you use must be references to actual HTML Elements in your template. For example, this reference will work just fine with GSAP:
<h1 ref="titleRef">{{ props.title }}</h1>
But this one won’t, because it’s referencing a Vue component instead of an HTML element:
<UButton ref="ctaButton" icon="lucide-zap" />
If you reference a Vue component, GSAP will try to apply the CSS animations on the component object directly. And a Vue component simply doesn’t have the API required for CSS animations and transforms. If you want to animate a component, I would recommend wrapping it in an HTML Element. It isn’t as elegant as I would want to, but it will have to be enough. Something like this:
<div ref="rightSideRef">
<NuxtImg :src="props.image" class="w-full h-[50svh] lg:h-[70svh] object-cover rounded-3xl" />
</div>
And basically, that’s all. I would have found this knowledge quite useful, and I hope that it saves you some time. If you have any suggestions or corrections, I’m always glad to learn!
Do you want to know more about Nuxtjs Vuejs GSAP