Why vue-animejs?
Anime.js is a powerful animation library, but using it raw inside Vue components leads to repetitive, error-prone boilerplate. vue-animejs removes that boilerplate by wiring Anime.js directly into Vue's reactivity system and component lifecycle.
Lifecycle cleanup
Animations started in onMounted must be stopped in onUnmounted, otherwise the instance keeps running and leaking memory after the component is destroyed.
<script setup lang="ts">
import { onMounted, onUnmounted, useTemplateRef } from "vue"
import anime from "animejs"
const box = useTemplateRef("box")
let animation: ReturnType<typeof anime.animate> | null = null
onMounted(() => {
animation = anime.animate(box.value, {
translateX: 200,
duration: 800,
loop: true,
})
})
// Easy to forget — and a memory leak if you do
onUnmounted(() => {
animation?.pause()
animation?.remove(box.value)
})
</script>
<template>
<div ref="box" class="box" />
</template><script setup lang="ts">
import { useTemplateRef } from "vue"
import { useAnimate } from "@juleshry/vue-animejs"
const box = useTemplateRef("box")
// Cleanup is automatic — no onMounted, no onUnmounted
useAnimate(box, {
translateX: 200,
duration: 800,
loop: true,
})
</script>
<template>
<div ref="box" class="box" />
</template>Reactive options
Updating an animation when reactive state changes requires manually restarting the instance. With vue-animejs, a Ref put straight into the options object is watched automatically, and so is a ref, computed or getter passed as the whole options object.
<script setup lang="ts">
import { ref, watch, onMounted, onUnmounted, useTemplateRef } from "vue"
import anime from "animejs"
const box = useTemplateRef("box")
const duration = ref(800)
let animation: ReturnType<typeof anime.animate> | null = null
function createAnimation() {
animation?.pause()
animation?.remove(box.value)
if (!box.value) return
animation = anime.animate(box.value, {
translateX: 200,
duration: duration.value, // raw value — won't update on its own
})
}
onMounted(createAnimation)
watch(duration, createAnimation) // manually reconnect each time
onUnmounted(() => {
animation?.pause()
animation?.remove(box.value)
})
</script>
<template>
<input v-model.number="duration" type="range" min="200" max="2000" />
<div ref="box" class="box" />
</template><script setup lang="ts">
import { ref, useTemplateRef } from "vue"
import { useAnimate } from "@juleshry/vue-animejs"
const box = useTemplateRef("box")
const duration = ref(800)
// Pass the ref directly — animation restarts whenever duration changes
useAnimate(box, { translateX: 200, duration })
</script>
<template>
<input v-model.number="duration" type="range" min="200" max="2000" />
<div ref="box" class="box" />
</template>Playback controls
Controlling an animation (play, pause, restart, seek) requires holding a reference to the Anime.js instance and calling methods on it imperatively.
<script setup lang="ts">
import { onMounted, onUnmounted, useTemplateRef } from "vue"
import anime from "animejs"
const box = useTemplateRef("box")
let animation: ReturnType<typeof anime.animate> | null = null
onMounted(() => {
animation = anime.animate(box.value, {
translateX: 200,
duration: 800,
autoplay: false,
})
})
onUnmounted(() => {
animation?.pause()
animation?.remove(box.value)
})
</script>
<template>
<div ref="box" class="box" />
<button @click="animation?.play()">Play</button>
<button @click="animation?.pause()">Pause</button>
<button @click="animation?.restart()">Restart</button>
</template><script setup lang="ts">
import { useTemplateRef } from "vue"
import { useAnimate } from "@juleshry/vue-animejs"
const box = useTemplateRef("box")
const { play, pause, restart } = useAnimate(box, {
translateX: 200,
duration: 800,
autoplay: false,
})
</script>
<template>
<div ref="box" class="box" />
<button @click="play">Play</button>
<button @click="pause">Pause</button>
<button @click="restart">Restart</button>
</template>SSR-safe
Raw Anime.js touches window and document as soon as you create an instance, so calling it from setup() crashes on the server. vue-animejs composables, directives and components are safe to use in Nuxt or any @vue/server-renderer app as-is. Nothing runs on the server, instances are created once the component mounts in the browser, and there is no <ClientOnly> to add.
v-animate, v-waapi and v-svg-drawable also render the animation's starting state on the server, so the element doesn't show its final state and then jump back when the page hydrates. v-text-split can hide its text until it's split, with ssrHidden: true. See SSR for details.
Declarative directives
Some animations don't need imperative control — they just need to run and clean up. For those cases, a directive skips the composable indirection entirely and animates straight from a template attribute.
<script setup lang="ts">
import { useTemplateRef } from "vue"
import { useAnimate } from "@juleshry/vue-animejs"
const box = useTemplateRef("box")
useAnimate(box, { translateX: 200, duration: 800, loop: true })
</script>
<template>
<div ref="box" class="box" />
</template><script setup lang="ts">
import { vAnimate } from "@juleshry/vue-animejs"
</script>
<template>
<div v-animate="{ translateX: 200, duration: 800, loop: true }" class="box" />
</template>Every composable that targets a single DOM element has a directive counterpart: v-animate, v-draggable, v-waapi, v-svg-drawable, and v-text-split. Reach for the composable instead when you need playback control (play, pause, restart) or access to the underlying instance.