Skip to content

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.

vue
<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>
vue
<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.

vue
<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>
vue
<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.

vue
<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>
vue
<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.

vue
<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>
vue
<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.