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.

importaossvg-scroll-draw/aos
version2.3.4 · 20222.23.3 · maintained
licenceMIT=MIT
gzip6.7 KB3.2 KB
in ReactYour own effectuseAOS · <AOSInit />
Fig. 1 — what changes, measured
01The problem

What goes wrong with the usual setup

  1. 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.
  2. 2React's StrictMode mounts, unmounts and remounts effects in development, so a plain useEffect(() => AOS.init(), []) runs init twice and nothing undoes the first.
  3. 3The useEffect has to live in a client component, so a layout needs a separate wrapper file just to start AOS.
  4. 4aos.css is a separate stylesheet you have to import globally.
023 steps

The switch, step by step

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

    terminal3 lines
    npx svg-scroll-draw migrate --dry-run
    npx svg-scroll-draw migrate
    npm install
  2. Step 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.tsx12 lines
    import { 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>
      );
    }
  3. 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.tsx8 lines
    'use client';
    import { usePathname } from 'next/navigation';
    import { useAOS } from 'svg-scroll-draw/aos/react';
    
    export function AOSWithRoute() {
      useAOS({ once: true }, usePathname());
      return null;
    }
03unchanged

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.
04questions

Questions

Q1

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

Q2

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.

Q3

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

Q4

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.

05More guides

More migration guides