useScroll
Demo
Scroll inside the box below — the square stays in view the whole time.
Wraps Anime.js onScroll() into a Vue composable. The ScrollObserver is created reactively — it re-creates whenever the container, target, or options change — and is reverted automatically on unmount.
Usage
Scroll-linked animation
Pass autoplayComputed straight into useTimeline (or useAnimate/useTimer) to drive an animation's progress directly from scroll position, instead of playing it once.
<script setup lang="ts">
import { useTemplateRef } from "vue"
import { useScroll, useTimeline } from "@juleshry/vue-animejs"
const container = useTemplateRef<HTMLDivElement>("container")
const target = useTemplateRef<HTMLDivElement>("target")
const box = useTemplateRef<HTMLDivElement>("box")
const { autoplayComputed } = useScroll(container, target, {
enter: "bottom top",
leave: "top bottom",
sync: true,
})
const { add } = useTimeline(autoplayComputed)
add(box, { translateX: 200, backgroundColor: "#9593ff", duration: 1000 })
</script>
<template>
<div ref="container" class="scroll-container">
<div ref="target" class="scroll-target" />
<div ref="box" class="box" />
</div>
</template>WARNING
Pass sync: true (or leave it unset for the default eased sync) so the timeline's progress scrubs with scroll position. Without sync, the ScrollObserver only starts the linked animation once, on enter, instead of tying its progress to scroll.
Reactive options
Pass a ref or computed as the third argument to change enter/leave/sync reactively. The underlying ScrollObserver is reverted and recreated whenever it changes, alongside the container/target.
<script setup lang="ts">
import { ref, computed, useTemplateRef } from "vue"
import { useScroll, useTimeline } from "@juleshry/vue-animejs"
const container = useTemplateRef<HTMLDivElement>("container")
const target = useTemplateRef<HTMLDivElement>("target")
const box = useTemplateRef<HTMLDivElement>("box")
const sync = ref(true)
const options = computed(() => ({ enter: "bottom top", leave: "top bottom", sync: sync.value }))
const { autoplayComputed } = useScroll(container, target, options)
const { add } = useTimeline(autoplayComputed)
add(box, { translateX: 200, duration: 1000 })
</script>
<template>
<div ref="container" class="scroll-container">
<div ref="target" class="scroll-target" />
<div ref="box" class="box" />
</div>
<label><input v-model="sync" type="checkbox" /> Sync to scroll</label>
</template>With useAnimate
Read autoplay and set it on a bigger reactive options object alongside whatever else you're animating, the same way you'd merge any other piece of reactive state.
<script setup lang="ts">
import { computed, useTemplateRef } from "vue"
import { useAnimate, useScroll } from "@juleshry/vue-animejs"
const container = useTemplateRef<HTMLDivElement>("container")
const target = useTemplateRef<HTMLDivElement>("target")
const box = useTemplateRef<HTMLDivElement>("box")
const { autoplay } = useScroll(container, target, {
enter: "bottom top",
leave: "top bottom",
sync: true,
})
const options = computed(() => ({
translateX: 200,
backgroundColor: "#9593ff",
duration: 1000,
autoplay: autoplay.value,
}))
useAnimate(box, options)
</script>
<template>
<div ref="container" class="scroll-container">
<div ref="target" class="scroll-target" />
<div ref="box" class="box" />
</div>
</template>With useRawAnimate
useRawAnimate creates its animation immediately and unconditionally from inside your own onMounted — it isn't reactive, so read .value off useScroll's autoplay at that point instead of passing autoplayComputed. useScroll itself still goes at the normal top level; only the useRawAnimate call needs to move into onMounted.
<script setup lang="ts">
import { onMounted, useTemplateRef } from "vue"
import { useRawAnimate, useScroll } from "@juleshry/vue-animejs"
const container = useTemplateRef<HTMLDivElement>("container")
const target = useTemplateRef<HTMLDivElement>("target")
const box = useTemplateRef<HTMLDivElement>("box")
const { autoplay } = useScroll(container, target, {
enter: "bottom top",
leave: "top bottom",
sync: true,
})
onMounted(() => {
useRawAnimate(box.value!, {
translateX: 200,
backgroundColor: "#9593ff",
duration: 1000,
autoplay: autoplay.value,
})
})
</script>
<template>
<div ref="container" class="scroll-container">
<div ref="target" class="scroll-target" />
<div ref="box" class="box" />
</div>
</template>Type Declarations
For all available options, see the Anime.js onScroll documentation.
Show Type Declarations
export interface UseScrollReturn {
/** The underlying Anime.js ScrollObserver. `undefined` until both container and target are available. */
observer: ComputedRef<ScrollObserver | undefined>
/** The observer, or `false` while it doesn't exist yet — plug straight into an `autoplay` option. */
autoplay: ComputedRef<ScrollObserver | false>
/** `{ autoplay }`, ready to pass straight as-is into `useTimeline`/`useTimer`/`useAnimate`'s options argument. */
autoplayComputed: ComputedRef<{ autoplay: ScrollObserver | false }>
/** Re-reads the container and target bounds. */
refresh: () => ScrollObserver | undefined
/** Cancels the observer and detaches its scroll listeners. */
revert: () => ScrollObserver | undefined
/** Renders the on-screen debug markers for the observer's thresholds. */
debug: () => void
/** Removes the on-screen debug markers. */
removeDebug: () => ScrollObserver | undefined
/** Links a tickable (animation, timer, timeline) or WAAPI animation to be driven by this observer. */
link: (linked: Tickable | WAAPIAnimation) => ScrollObserver | undefined
}
/**
* Wraps Anime.js `onScroll()` into a Vue composable. Reactively re-creates the ScrollObserver
* when the container, target, or options change, and reverts it automatically on unmount.
*
* @param container - The scrollable container. Accepts a template ref, a CSS selector, a DOM element, or a reactive ref to any of these.
* @param target - The element whose scroll position is observed within `container`. Same accepted shapes as `container`.
* @param options - Anime.js `ScrollObserverParams`, minus `container`/`target`. Accepts a plain object or a reactive ref / computed. Defaults to `{}`.
*/
export declare function useScroll(
container: AnimationTargets,
target: AnimationTargets,
options?: MaybeRef<Omit<ScrollObserverParams, "container" | "target">>
): UseScrollReturnReactivity & Lifecycle Behavior
- Unlike other composables,
observer/autoplay/autoplayComputedare computed, not eagerly created on mount — nothing is created until one of them is read. This keeps the resolvedScrollObserveridentical across every reader within the same reactive tick (for example a consuminguseTimeline's own mount-time watch andtryOnMounted), instead of exposing a transientundefinedthat flips to the real observer a tick later and makes the consumer build twice. - The observer is created the moment
containerandtargetboth resolve to a real element. Readingobserver/autoplay/autoplayComputedbefore then returnsundefined/false/{ autoplay: false }. - If
container,target, oroptionschanges, the previous observer is reverted before the new one is created. - On component unmount, the last created observer is reverted automatically.
- Safe to call during SSR — the observer is never created server-side (
window/documentaren't touched), regardless of whetherobserver/autoplayis read.
TIP
autoplayComputed exists so you don't have to write computed(() => ({ autoplay: autoplay.value })) yourself. If you need to combine it with other options, wrap it in your own computed instead: computed(() => ({ ...autoplayComputed.value, loop: true })).
WARNING
observer is a readonly computed ref. Do not mutate the Anime.js instance directly — use the returned control methods instead.