NextJSAppRouterDebuggingUXCSS

Why My Next.js Site Got a Scroll Animation Every Time I Navigated Pages

May 15, 20261 min read

Some weird animation showed up on my blog.

Clicking a post from the listing made the detail page slide up from the bottom, and pressing the ← cd .. button on the detail page made the list page slide down from the top. I definitely never added this animation.

Tracing the cause turned up two bugs. One was how the back button was implemented, the other was a single line of global CSS.


The full picture of the symptom

Navigation directionBehavior
List → click a detail pagedetail page scrolls from the bottom up
Detail → click ← cd ..list page scrolls from the top down

At a glance, it looks like a deliberately added scroll animation. It wasn't.


Cause 1 — scroll-behavior: smooth

First, I opened globals.css.

/* globals.css */
html { scroll-behavior: smooth; }

This one line was the core cause.

Set scroll-behavior: smooth globally on html, and the animation applies even when JavaScript programmatically moves the scroll position — not just when the user scrolls manually.

Next.js App Router programmatically handles two things when navigating:

  • Moving to a new page: window.scrollTo(0, 0) — this should jump to the top instantly, but the smooth animation applies instead
  • Going back to a previous page: restoring the saved scroll position — the smooth animation applies all the way to the restored position too

So every page navigation ended up looking like a smooth sliding scroll.

Fix: change to scroll-behavior: auto

/* globals.css */
html { scroll-behavior: auto; }  /* smooth → auto */

With this, Next.js's programmatic scroll jumps instantly, and only the user's own manual scrolling follows the default behavior.


Cause 2 — <Link href="/posts"> isn't going back

After fixing the CSS, the scroll animation was gone. But pressing the ← cd .. button still felt off — the list page felt like it was loading from scratch.

Looking at the code, it was implemented like this:

// existing code
<Link href="/posts">← cd ..</Link>

Looks like going back at a glance, but this isn't the browser's "back" — it's a fresh navigation to /posts. It doesn't step backward through browser history; it adds a new history entry.

So the list page behaved like this:

1. Click the Link → /posts renders completely from scratch
2. Server component: re-fetches the post list via the GitHub API
3. Data arrives → post cards get added to the DOM → page height grows
4. Next.js: "this page was previously scrolled to 600px, so let's restore that"
5. Scrolls to 600px (with smooth removed, now an instant jump)

Removing smooth got rid of the animation, but the scroll-restoration itself still happened.

Fix: swap it for router.back()

Just use actual back navigation. It pops the previous page off the browser history stack and restores it, pulling from cache rather than a fresh render. The scroll position stays exactly as it was.

useRouter is a client hook and can't be used directly in a server component. Split it into a small client component.

// components/BackButton.tsx
"use client";

import { useRouter } from "next/navigation";

export default function BackButton({ label = "← cd .." }: { label?: string }) {
  const router = useRouter();

  return (
    <button
      onClick={() => router.back()}
      style={{
        background: "none",
        border: "none",
        cursor: "pointer",
        padding: 0,
        color: "hsl(var(--muted-foreground))",
        fontFamily: "var(--font-mono), monospace",
      }}
    >
      {label}
    </button>
  );
}

Swap it in on the post detail page (a server component) like this.

// Before
import { Link } from "@/i18n/navigation";
<Link href="/posts">← cd ..</Link>

// After
import BackButton from "@/components/BackButton";
<BackButton />

Link vs. router.back() — which to use, when

SituationUse
A link that always goes to the same page<Link href="...">
Something reachable from various pathsrouter.back()
Need to preserve the scroll position of the previous pagerouter.back()
A link needed for SEO (crawlers need to follow it)<Link href="...">

Going back from a post detail page to the list is a textbook router.back() case. You might've come from the list, or you might've landed on the post directly via URL — either way, the intent behind "go back" is "return to wherever I just was."


Troubleshooting

I removed smooth, and now anchor links (#section) lost their smooth scrolling

That's because scroll-behavior: smooth was removed globally. If you need smooth scrolling specifically for anchor navigation, handle it explicitly in the link's click handler.

// when you need smooth scrolling for an anchor link
element.scrollIntoView({ behavior: 'smooth' })

Applying it selectively where needed, rather than globally in CSS, is the safer approach.

Pressing router.back() does nothing

This happens when there's no previous page in browser history (e.g., the user arrived by typing the URL directly). Add a fallback.

onClick={() => {
  if (window.history.length > 1) {
    router.back()
  } else {
    router.push('/posts')
  }
}}

Summary

The cause and fix for both bugs, summarized:

Bug 1 — scroll-behavior: smooth
  Cause: html { scroll-behavior: smooth } in globals.css
  Effect: Next.js's programmatic scrollTo gets animated too
  Fix: changed to scroll-behavior: auto

Bug 2 — Link href vs. router.back()
  Cause: <Link href="/posts"> = fresh navigation (adds to history)
  Effect: list page re-renders + scroll-position restoration jump
  Fix: router.back() = browser history restoration (instant, from cache)

Setting scroll-behavior: smooth globally on html produces animation in places you never expect — especially in a framework like Next.js, where page navigation is handled programmatically via JavaScript. It's better to apply it explicitly only where it's actually needed.

PM

backtodev

A 40-something PM returns to code. Learning, failing, and growing.

Why My Next.js Site Got a Scroll Animation Every Time I Navigated Pages | backtodev