guide / aos in next.js3 steps
AOS in the App Router, set up once.
Most AOS setups in Next.js are a client component that calls AOS.init() inside a useEffect. It works, but AOS was written before React: it has no way to stop. svg-scroll-draw/aos is a drop-in that keeps the AOS API and your data-aos markup, adds destroy(), and ships a "use client" component you can put straight into app/layout.tsx.
What goes wrong with the usual setup
- 1AOS 2.3.4 has init, refresh and refreshHard — no destroy. Every init() adds its scroll, resize and orientation listeners and starts another MutationObserver, and nothing removes them.
- 2React's StrictMode mounts, unmounts and remounts effects in development, so a plain useEffect(() => AOS.init(), []) runs init twice and nothing undoes the first.
- 3The useEffect has to live in a client component, so a layout needs a separate wrapper file just to start AOS.
- 4aos.css is a separate stylesheet you have to import globally.
The switch, step by step
Step 01 · terminal
Switch the package — one command
This rewrites the aos import, removes the aos.css import and swaps the dependency in package.json. Preview with --dry-run first; it refuses to write over uncommitted git changes.
terminalnpx svg-scroll-draw migrate --dry-run npx svg-scroll-draw migrate npm installStep 02 · app/layout.tsx
Start AOS from the layout
AOSInit is a client component, so a Server Component layout can render it directly. It initialises on mount and destroys on unmount, which makes StrictMode’s double mount harmless. Delete your old wrapper component and its useEffect.
app/layout.tsximport { AOSInit } from 'svg-scroll-draw/aos/react'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <AOSInit once duration={700} /> {children} </body> </html> ); }Step 03 · app/aos-with-route.tsx
Re-measure when the layout moves (optional)
New [data-aos] elements that a navigation renders are picked up automatically by a MutationObserver. If something else shifts the page — images loading, data arriving — pass a refreshKey and positions are measured again when it changes.
app/aos-with-route.tsx'use client'; import { usePathname } from 'next/navigation'; import { useAOS } from 'svg-scroll-draw/aos/react'; export function AOSWithRoute() { useAOS({ once: true }, usePathname()); return null; }
What stays exactly the same
- Every data-aos, data-aos-offset, data-aos-delay, data-aos-duration, data-aos-easing and data-aos-anchor-placement attribute.
- The animations themselves: the drop-in mirrors aos.css 2.3.4 rule for rule, so custom [data-aos="…"] CSS keeps working.
- Trigger positions: the browser test suite runs AOS 2.3.4 beside the drop-in and checks they animate the same elements at the same scroll positions.
Questions
Do I still need aos.css?
No. The drop-in injects the same rules itself when AOS starts. Remove the import (npx svg-scroll-draw migrate does it for you).
Does it work with the Pages Router too?
Yes. Render <AOSInit /> in pages/_app.tsx, or call useAOS() in a component that mounts on every page.
Will it run on the server?
No — it renders nothing on the server and starts in the browser after hydration. Importing it in a Server Component file is fine because the entry is marked "use client".
What about users who prefer reduced motion?
With prefers-reduced-motion: reduce, every element is shown in its final state immediately. AOS itself ignores that setting.