Performance
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:
- Animations and performance
- Avoid large, complex layouts and layout thrashing
- Stick to compositor-only properties
- prefers-reduced-motion
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
morphToand motion paths withcreateMotionPath - 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>