Getting Started

Performance

Choosing the right composable and options for animations that stay smooth.

Most animation performance advice is about the browser, not any library. Which properties are cheap, what forces a reflow, what the compositor can do on its own. That ground is covered well elsewhere:

Short version: animate translateX, translateY, scale, rotate and opacity, leave width and top alone. The rest of this page is about nanime itself.

Choosing between the two engines

useWaapiAnimate hands the animation to the browser's Web Animations API. For transform and opacity the browser runs it on the compositor thread, so it holds its frame rate while the main thread is busy hydrating or fetching, and costs less CPU and battery. Modals, dropdowns, page entrances and ambient loops all fit.

<script setup lang="ts">
const modal = useTemplateRef('modal')

useWaapiAnimate(modal, {
  transform: ['translateY(20px) scale(0.95)', 'translateY(0px) scale(1)'],
  opacity: [0, 1],
  duration: 300,
  easing: 'cubic-bezier(0.2, 0, 0, 1)',
})
</script>

useAnimate runs on the main thread through requestAnimationFrame, so it competes with everything else happening there. Reach for it when you need something WAAPI has no way to express:

  • SVG morphing with morphTo and motion paths with createMotionPath
  • SVG line drawing with createDrawable
  • Text scrambling through useScrambleText
  • Animating plain JavaScript objects and numbers
  • Stagger maths and grid distribution

Mixing them in one component is fine. The loading sequence spins its icon with useWaapiAnimate so it keeps turning while useAnimate works on the main thread.

Driving fast updates with useAnimatable

Cursor tracking, scroll handlers and drag events fire many times a second. Recreating an animation on each one means rebuilding an instance dozens of times before a single one finishes.

useAnimatable creates one instance up front and exposes each property as a setter. The handler sets a new target value, and the composable tweens towards it from wherever the property currently sits.

<script setup lang="ts">
const circle = useTemplateRef('circle')

const animatable = useAnimatable(circle, {
  translateX: 0,
  translateY: 0,
  ease: 'outExpo',
  duration: 300,
})

function onMouseMove(event: MouseEvent) {
  animatable.translateX(event.clientX)
  animatable.translateY(event.clientY)
}
</script>

A getter would work here too, but every mouse move would revert the running animation and build a new one. Getters are for values that change occasionally, like a prop or a media query. A plain object is enough when nothing changes at all.

Keeping the returned instance intact

The animation composables return their instance wrapped in a shallowReactive object, which is what makes progress and paused readable in a template. Destructuring reads those once and hands back plain values, disconnecting them from the instance.

// loses reactivity
const { play, pause, progress } = useAnimate(box, { ... })

// keeps it
const animation = useAnimate(box, { ... })
animation.play()

Handing composables an element rather than a selector

Targets can be a selector string, a component instance or a template ref. Inside a component that renders more than once on a page, '.card' resolves against the whole document, so one instance animates another's elements. A template ref points at one element and needs no lookup.

<script setup lang="ts">
const card = useTemplateRef('card')

useAnimate(card, {
  scale: [0.9, 1],
  opacity: [0, 1],
  duration: 400,
})
</script>

<template>
  <div ref="card" class="card">Content</div>
</template>

Keeping split text short

useSplitText wraps each unit in its own element. A heading split into characters is a few dozen spans. An article split the same way is several thousand for the browser to style and lay out.

Split headings, hero titles and short callouts, and prefer { words: true } or { lines: true } over { chars: true } where the effect still reads. The composable returns a lines, words and chars array, so moving between levels is a change of option and target:

<script setup lang="ts">
import { stagger } from '#nanime/utils'

const heading = useTemplateRef('heading')

// one span per character on a short heading
const { chars } = useSplitText(heading, { chars: true })

useAnimate(chars, {
  translateY: [20, 0],
  opacity: [0, 1],
  delay: stagger(40),
  duration: 400,
  ease: 'outQuad',
})
</script>

<template>
  <h2 ref="heading">Ship it</h2>
</template>
Copyright © 2026