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.

FadeCard.tsx
tsx
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:

useMotion.ts
ts
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:

tsx
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.

FadeCard.vue
vue
<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:

ts
// 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:

HeroSection.tsx
tsx
"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:

tsx
"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

astro
---
// 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:

astro
<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

MethodWhen 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:

ts
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();