SPA Integration
Use the SDK with React, Vue, Next.js, and Astro — cleanup, page transitions, lifecycle.
The SDK is client-side JavaScript — it must run in the browser and always requires cleanup when components unmount or routes change. Each framework has its own lifecycle hooks for this.
React
Use useEffect to create animations after mount and return a cleanup function that kills them on unmount. Target elements with useRef to avoid stale selectors.
import { useEffect, useRef } from "react";
import { Motion } from "@motion.page/sdk";
export function FadeCard() {
const cardRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!cardRef.current) return;
Motion("fade-card", cardRef.current, {
from: { opacity: 0, y: 24 },
duration: 0.5,
ease: "power2.out",
}).onPageLoad();
return () => {
Motion.kill("fade-card");
};
}, []);
return <div ref={cardRef}>Hello</div>;
} Use Motion.kill(name) to remove a specific timeline, or Motion.killAll() to clear every timeline registered on the page.
Shared cleanup hook
Extract cleanup into a reusable hook when multiple components need the same pattern:
import { useEffect } from "react";
import { Motion } from "@motion.page/sdk";
export function useMotion(setup: () => void, deps: React.DependencyList = []) {
useEffect(() => {
setup();
return () => Motion.killAll();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
} Scroll triggers after dynamic content
If your component renders a list from async data, call Motion.refreshScrollTriggers() after the data loads so scroll positions are recalculated:
useEffect(() => {
if (!items.length) return;
Motion("list-reveal", ".list-item", {
from: { opacity: 0, y: 20 },
duration: 0.4,
stagger: 0.08,
}).onScroll({ each: true });
Motion.refreshScrollTriggers();
return () => Motion.kill("list-reveal");
}, [items]); Vue
Use onMounted and onUnmounted to manage the animation lifecycle. Access elements with ref template refs.
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { Motion } from "@motion.page/sdk";
const card = ref<HTMLElement | null>(null);
onMounted(() => {
if (!card.value) return;
Motion("fade-card", card.value, {
from: { opacity: 0, y: 24 },
duration: 0.5,
ease: "power2.out",
}).onPageLoad();
});
onUnmounted(() => {
Motion.kill("fade-card");
});
</script>
<template>
<div ref="card">Hello</div>
</template> Vue Router page transitions
Kill all timelines on route leave so animations don’t leak between pages:
// router/index.ts
import { Motion } from "@motion.page/sdk";
router.afterEach(() => {
Motion.killAll();
}); Next.js
The SDK is client-side only — it cannot run in Server Components. Any file that imports Motion must be a Client Component.
App Router
Add "use client" at the top of any component that uses the SDK:
"use client";
import { useEffect } from "react";
import { Motion } from "@motion.page/sdk";
export function HeroSection() {
useEffect(() => {
Motion("hero-fade", "#hero-heading", {
from: { opacity: 0, y: 32 },
duration: 0.7,
ease: "power3.out",
}).onPageLoad();
return () => Motion.kill("hero-fade");
}, []);
return <h1 id="hero-heading">Welcome</h1>;
} Page transitions with App Router
Next.js App Router navigations unmount the previous page tree. The useEffect cleanup above handles this automatically — no extra route-change listener needed.
For shared layout components that persist across navigations (e.g. a nav bar), use pathname as a dependency to reinitialise:
"use client";
import { useEffect } from "react";
import { usePathname } from "next/navigation";
import { Motion } from "@motion.page/sdk";
export function PageTransition({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
useEffect(() => {
Motion("page-in", "main", {
from: { opacity: 0 },
duration: 0.35,
}).onPageLoad();
return () => Motion.kill("page-in");
}, [pathname]);
return <>{children}</>;
} Astro
Add client:load to any Astro component that uses the SDK so it hydrates in the browser. Pure .astro files can run the SDK in an inline <script> tag.
Inline script
---
// HeroSection.astro
---
<section id="hero">
<h1 class="hero-heading">Welcome</h1>
</section>
<script>
import { Motion } from "@motion.page/sdk";
Motion("hero-fade", ".hero-heading", {
from: { opacity: 0, y: 32 },
duration: 0.7,
ease: "power3.out",
}).onPageLoad();
</script> View Transitions API
Astro’s View Transitions dispatch astro:page-load and astro:before-swap events. Kill animations before the swap and re-initialise on each new page load:
<script>
import { Motion } from "@motion.page/sdk";
function initAnimations() {
Motion("page-reveal", ".animate-in", {
from: { opacity: 0, y: 20 },
duration: 0.5,
stagger: 0.1,
}).onPageLoad();
}
// Initial load
initAnimations();
// Re-run after each View Transition navigation
document.addEventListener("astro:page-load", () => {
initAnimations();
});
// Clean up before the DOM is swapped out
document.addEventListener("astro:before-swap", () => {
Motion.killAll();
});
</script> Cleanup reference
| Method | When to use |
|---|---|
Motion.kill("name") | Remove one specific timeline by name |
Motion.killAll() | Remove all timelines — use on route change or full page teardown |
Motion.refreshScrollTriggers() | Recalculate scroll positions after dynamic content renders |
Motion.context(() => { ... }) | Scope multiple timelines so they can all be torn down together |
Motion.context
Motion.context is useful when a component creates several timelines and you want a single cleanup call:
import { Motion } from "@motion.page/sdk";
const ctx = Motion.context(() => {
Motion("nav-fade", ".nav-item", {
from: { opacity: 0, x: -12 },
stagger: 0.06,
duration: 0.4,
}).onPageLoad();
Motion("nav-underline", ".nav-link", {
to: { scaleX: 1 },
duration: 0.25,
}).onHover({ onLeave: "reverse" });
});
// On cleanup — kills both timelines
ctx.revert(); Related
- Timeline Control —
Motion.get(),Motion.has(), manual play/pause - Page Load —
.onPageLoad()trigger options - Scroll Trigger —
.onScroll()andrefreshScrollTriggers()