useAnimeScroll
useAnimeScroll wraps the onScroll
function from AnimeJS. It plays or scrubs an animation as an element scrolls
through its container. Pass the result as an animation's autoplay.
nanime creates the observer once the component mounts and reverts it on
unmount. It rebuilds the observer when target or container points to a
new element.
Usage
import { createDrawable } from '#nanime/proxies/svg'
import type { DrawableSVGGeometry } from '#nanime/types'
const THREAD = 'M 0 100 C 300 20 560 180 860 100 S 1420 18 1720 104 S 2150 192 2300 120'
const panel = useTemplateRef<HTMLElement>('panel')
const track = useTemplateRef<HTMLElement>('track')
const thread = useTemplateRef<SVGPathElement>('thread')
const scroll = useAnimeScroll({
container: panel,
target: track,
axis: 'x',
enter: 'left left',
leave: 'right right',
sync: true,
})
const drawable = computed<DrawableSVGGeometry | null>(() =>
thread.value ? createDrawable(thread.value)[0] ?? null : null,
)
useAnimate([drawable], {
draw: ['0 0', '0 1'],
ease: 'linear',
autoplay: scroll,
})
<template>
<div ref="panel" class="...">
<div ref="track" class="relative w-[2400px] h-full flex items-center">
<svg viewBox="0 0 2400 200" class="w-[2400px] h-[200px] text-primary">
<path
:d="THREAD"
class="stroke-current/15"
fill="none"
stroke-width="3"
stroke-linecap="round"
/>
<path
ref="thread"
:d="THREAD"
class="stroke-current"
fill="none"
stroke-width="3"
stroke-linecap="round"
/>
</svg>
<Icon name="lucide:lightbulb" class="..." style="left: 2300px" />
</div>
</div>
</template>
Signature
function useAnimeScroll(
parameters?: AnimeScrollParams
): BufferedProxyReturns<ScrollObserver>
Parameters
target and container also accept template refs and component refs,
and a new element rebuilds the observer. enter, leave, repeat and
axis also accept a ref or computed ref, and a change updates the
observer in place. A function that reads a ref
does not.Returns
Returns a buffered proxy around the AnimeJS ScrollObserver.
Its methods and properties are in the
AnimeJS scroll documentation.
The members nanime changes:
| Member | Type | Notes |
|---|---|---|
link(animation) | (animation) => this | animation also accepts the value returned by useAnimate or useWaapiAnimate. The link survives a rebuild. |
progress | number | Read it after mount. It doesn't update a template. |
The observer is created when the component mounts. link(), refresh()
and revert() can be called straight from setup(), and run at that
point. An observer drives one animation, so create a useAnimeScroll for
each animation you pass it to.
Caveats
Pass the template ref, not its value
// wrong: reads template ref during setup before mount
const section = useTemplateRef('section')
const scroll = useAnimeScroll({ target: section.value })
// right: pass the ref itself so it resolves after mount
const scroll = useAnimeScroll({ target: section })
Template refs evaluate to null during setup before the DOM renders. Passing
section.value reads it immediately as null. AnimeJS then falls back to
document.body as the tracked target instead of the intended element. Pass
the ref itself so target and container resolve after mount.
Pass a ref, not a function that reads one
const isMobile = ref(false)
// wrong: the observer never sees isMobile change
const scroll = useAnimeScroll({ enter: () => isMobile.value ? 'bottom top' : 'center top' })
// right: a computed ref updates the observer in place
const enter = computed(() => isMobile.value ? 'bottom top' : 'center top')
const scroll = useAnimeScroll({ enter })
A function for enter, leave, repeat or axis is an AnimeJS callback.
AnimeJS calls it with the observer when it refreshes, on resize for example,
so refs read inside it are not tracked. Pass a ref or computed ref to update
the observer when a value changes.
Read properties after mount
// wrong: progress is undefined before mount
const scroll = useAnimeScroll({ target: section })
console.log(scroll.progress) // undefined
// right: read it after mount, or in a callback
onMounted(() => {
console.log(scroll.progress)
})
The observer is created after the component mounts and resolves target.
Chainable methods such as link() are queued until then, but property
reads return undefined. Read properties after mount or inside a callback
such as onUpdate.
Properties do not update templates
// wrong: progress never updates inside a template interpolation
// <p>{{ scroll.progress }}</p>
// right: track progress via a ref updated in onUpdate
const progress = ref(0)
const scroll = useAnimeScroll({
onUpdate: observer => progress.value = observer.progress,
})
Interpolating {{ scroll.progress }} displays the initial value and never
updates. The returned proxy tracks the observer instance rather than internal
numeric state, so scrolling triggers no component re-renders. Update a
ref inside the onUpdate callback to display live scroll
progress.
Type reference
type ScrollElement = HTMLElement | SVGElement | ComponentPublicInstance | null | undefined
type AnimeScrollTarget = TargetsParam | MaybeRef<ScrollElement> | MaybeRef<ScrollElement>[]
interface AnimeScrollParams extends Omit<ScrollObserverParams, 'target' | 'container' | 'enter' | 'leave' | 'repeat' | 'axis'> {
target?: AnimeScrollTarget
container?: AnimeScrollTarget
enter?: ScrollThresholdValue | ScrollThresholdParam | ScrollThresholdCallback | Readonly<Ref<ScrollThresholdValue | ScrollThresholdParam>>
leave?: ScrollThresholdValue | ScrollThresholdParam | ScrollThresholdCallback | Readonly<Ref<ScrollThresholdValue | ScrollThresholdParam>>
repeat?: boolean | ((observer: ScrollObserver) => boolean) | Readonly<Ref<boolean>>
axis?: 'x' | 'y' | ScrollObserverAxisCallback | Readonly<Ref<'x' | 'y'>>
}
type BufferedProxyReturns<T> = Exclude<UnwrapNestedRefs<T>, null | undefined>
See also
useAnimate: main-thread animation engine supporting scroll autoplay- Road trip: a car driven along a winding road by a panel's horizontal scroll
- AnimeJS scroll documentation: upstream threshold syntax and container parameters