proscinski.com

Poznaj problem z html { scroll-behavior: smooth } w Next.js App Router i dowiedz się, jak prawidłowo obsłużyć płynne scrollowanie bez psujących efektów.

Dodajesz html { scroll-behavior: smooth } do globalnych styli, bo chcesz, żeby kliknięcie w odnośnik z #id ładnie przewijało stronę do odpowiedniej sekcji. Wygląda niewinnie – jedna linijka CSS, standard W3C, działa we wszystkich nowoczesnych przeglądarkach. Problem pojawia się w momencie, gdy używasz Next.js App Router: przy każdej zmianie strony przeglądarka wykonuje animowany scroll do góry, zamiast od razu pokazać nową stronę. Efekt jest irytujący, czasem scroll nawet nie dochodzi do samego początku strony, a użytkownik widzi dziwne przeskakiwanie treści.

Jak działa scroll-behavior: smooth

Właściwość CSS scroll-behavior kontroluje, jak przeglądarka wykonuje każde programatyczne przewinięcie – nie tylko kliknięcia w anchory. Dotyczy to również wywołań window.scrollTo(), window.scroll()element.scrollIntoView() bez jawnie ustawionego parametru behavior.

Gdy ustawisz scroll-behavior: smooth na elemencie html, wpływa to na każdy scroll w całym dokumencie. To kluczowa informacja, która wyjaśnia dlaczego psuje to nawigację w Next.js.

Pozornie nieszkodliwa reguła w globals.css
CSS
/* globals.css */
html {
  scroll-behavior: smooth;
}

Dlaczego to psuje nawigację w Next.js

Next.js App Router po każdej nawigacji między stronami wywołuje wewnętrznie window.scrollTo(0, 0), żeby nowa strona zaczynała się od góry. To standardowe i pożądane zachowanie – nikt nie chce wylądować na nowej podstronie w połowie treści.

Problem polega na tym, że gdy scroll-behavior: smooth jest ustawione globalnie, to scrollTo(0, 0) wykonywane przez Next.js również zostaje animowane. Zamiast natychmiastowego przeskoczenia na górę strony, przeglądarka płynnie przewija stronę do pozycji zero.

W praktyce daje to trzy zauważalne problemy:

  • Widoczna animacja scrollowania – przy każdej zmianie strony użytkownik widzi, jak treść przewija się do góry. Na długich stronach trwa to nawet sekundę.
  • Scroll nie dochodzi do końca – jeśli nowa strona jest krótsza od poprzedniej, przeglądarka zaczyna animację scrollowania, ale w trakcie zmienia się wysokość dokumentu. Animacja kończy się przedwcześnie i strona zatrzymuje się kilkadziesiąt pikseli od góry.
  • Wyścig z renderowaniem – Next.js renderuje nową stronę i jednocześnie trwa animacja scrollowania ze starej pozycji. Oba procesy walczą o pozycję scrolla, co powoduje nienaturalne przeskakiwanie treści.

Rozwiązanie: usuń globalne scroll-behavior

Najczystsze rozwiązanie to całkowite usunięcie scroll-behavior: smooth z elementu html. Nie próbuj obchodzić problemu hackami w JavaScript, tymczasowym nadpisywaniem styli ani przechwytywaniem eventów routera. Po prostu usuń tę jedną regułę.

globals.css – przed zmianą
CSS
/* ❌ Nie rób tego w Next.js */
html {
  scroll-behavior: smooth;
}
globals.css – po zmianie
CSS
/* ✅ Usuń scroll-behavior z html */
html {
  /* scroll-behavior: smooth; ← usunięte */
}

A co z płynnym scrollowaniem do sekcji?

Skoro usuwamy scroll-behavior: smooth z CSS, jak obsłużyć płynne przewijanie do elementu z #id? Odpowiedź: za pomocą JavaScript i metody scrollIntoView().

Zamiast polegać na globalnej regule CSS, która wpływa na każdy scroll w aplikacji, wywołaj scrollIntoView({ behavior: "smooth" }) tylko tam, gdzie faktycznie chcesz animacji. To daje pełną kontrolę – decydujesz, który scroll jest płynny, a który natychmiastowy.

Płynne scrollowanie do elementu – scrollIntoView
TypeScript
function scrollToSection(id: string) {
  document.getElementById(id)?.scrollIntoView({
    behavior: "smooth",
    block: "start",
  });
}

Parametr behavior: "smooth" przekazany bezpośrednio do scrollIntoView() działa niezależnie od wartości scroll-behavior w CSS. Nawet jeśli globalnie scroll jest ustawiony na auto (domyślna wartość), wywołanie z behavior: "smooth" animuje przewijanie.

Przykład: spis treści z płynnym scrollowaniem

Typowy przypadek użycia to spis treści na blogu, który po kliknięciu przewija do odpowiedniego nagłówka. Oto jak to zaimplementować w Next.js bez globalnego scroll-behavior: smooth:

Komponent spisu treści z płynnym scrollowaniem
TypeScript
"use client";

interface TocItem {
  id: string;
  text: string;
  level: number;
}

export function TableOfContents({ items }: { items: TocItem[] }) {
  const handleClick = (e: React.MouseEvent, id: string) => {
    e.preventDefault();
    document.getElementById(id)?.scrollIntoView({
      behavior: "smooth",
      block: "start",
    });
    // Opcjonalnie: zaktualizuj URL bez przeładowania
    window.history.pushState(null, "", `#${id}`);
  };

  return (
    <nav>
      <ul>
        {items.map((item) => (
          <li key={item.id}>
            <a
              href={`#${item.id}`}
              onClick={(e) => handleClick(e, item.id)}
            >
              {item.text}
            </a>
          </li>
        ))}
      </ul>
    </nav>
  );
}

Przykład: przycisk „wróć na górę”

Kolejny popularny przypadek – przycisk, który płynnie przewija stronę na samą górę. Zamiast polegać na scroll-behavior: smooth w CSS i window.scrollTo(0, 0), użyj scrollTo z jawnym parametrem behavior:

Przycisk „wróć na górę”
TypeScript
function scrollToTop() {
  window.scrollTo({
    top: 0,
    behavior: "smooth",
  });
}

Ta wersja scrollTo() przyjmuje obiekt z parametrem behavior, który – tak samo jak w scrollIntoView() – nadpisuje globalne ustawienie CSS. Dzięki temu scroll w tym konkretnym miejscu jest płynny, ale Next.js wewnętrznie nadal może robić natychmiastowy scrollTo(0, 0) przy nawigacji.

Porównanie obu podejść

Poniższa tabela zestawia globalne scroll-behavior: smooth z podejściem opartym o scrollIntoView() w kontekście Next.js:

scroll-behavior: smooth (CSS)scrollIntoView() (JS)
Wpływ na nawigację Next.jsAnimuje każdy scrollTo() – w tym wewnętrzny scroll routeraNie wpływa na nawigację – router scrolluje natychmiastowo
KontrolaGlobalna – wszystko albo nicPrecyzyjna – tylko tam, gdzie wywołasz
Anchory (#id)Automatycznie płynne dla wszystkich linkówWymagają obsługi w JavaScript
Kompatybilność z frameworkamiKonfliktuje z wewnętrznym scrollem Next.js, Remix, AstroBezkonfliktowe – nie zmienia globalnego zachowania
Ilość kodu1 linijka CSSKilka linijek JS w każdym miejscu użycia

Opcja pośrednia: scroll-behavior tylko na wybranym kontenerze

Jeśli masz sekcję strony, w której wiele elementów wymaga płynnego scrollowania (np. karuzela lub dokumentacja z wieloma anchorami), możesz ustawić scroll-behavior: smooth na konkretnym kontenerze zamiast na html:

scroll-behavior: smooth na kontenerze zamiast globalnie
CSS
/* Tylko wybrany kontener scrolluje płynnie */
.docs-sidebar {
  overflow-y: auto;
  scroll-behavior: smooth;
}

To podejście nie wpływa na scroll głównego dokumentu (html), więc nawigacja Next.js działa natychmiastowo. Jednocześnie wewnątrz kontenera kliknięcia w anchory i programatyczne scrolle są płynne bez dodatkowego JavaScriptu.

Podsumowanie

html { scroll-behavior: smooth } to jedna z tych reguł CSS, które wyglądają na idealny skrót, ale w aplikacjach z client-side routingiem powodują więcej problemów niż korzyści. W Next.js App Router prowadzą do irytującego animowanego scrollowania przy każdej nawigacji, scrollu który nie dociera do góry strony i nienaturalnego przeskakiwania treści.

Rozwiązanie jest proste:

  1. Usuń scroll-behavior: smooth z globalnych styli.
  2. Tam, gdzie potrzebujesz płynnego scrollowania, użyj scrollIntoView({ behavior: "smooth" }) lub scrollTo({ top: 0, behavior: "smooth" }).
  3. Opcjonalnie – ustaw scroll-behavior: smooth na konkretnym kontenerze CSS, a nie na html.

Jedna usunięta linijka CSS. Pełna kontrola nad scrollowaniem. Zero konfliktów z routerem.

Chcesz wdrożyć podobną stronę albo poprawić SEO?

Pracuję lokalnie w Lublin i zdalnie w całej Polsce. Jeśli ten temat dotyczy Twojej strony, poniżej masz dwa sensowne następne kroki.

Główny landing dla firm usługowych, które chcą więcej telefonów i formularzy.

TelefonBezpłatna wycena