next.jsanimatiescss en layouts

View Transitions in Next.js: paginaanimaties zonder animatiebibliotheek

View Transitions in Next.js: paginaanimaties zonder animatiebibliotheek

In het laatste artikel liet ik zien dat INP het verschil meet tussen een klik en het frame dat die weergeeft, en dat één lange taak op de hoofdthread dat verschil kan ruïneren. Paginatransities zijn de extreme versie van dat probleem: ze vragen de hoofdthread zijn duurste werk te doen — een hele nieuwe pagina renderen — en tegelijkertijd een vloeiende 60fps-animatie te draaien.

Wat de bibliotheken echt deden: FLIP

In 2015 beschreef Paul Lewis een techniek die hij FLIP noemde – First, Last, Invert, Play. De View Transitions API neemt die truc over en geeft hem aan de browser. In plaats van twee levende DOM-bomen bij te houden, fotografeert de browser de oude staat, laat het DOM bijwerken, fotografeert de nieuwe staat en animeert tussen de twee afbeeldingen – op de compositor.

javascript

function navigate(update) {
if (!document.startViewTransition) {
update(); // geen ondersteuning: gewoon bijwerken, geen animatie
return;
}
document.startViewTransition(() => update());
}

css

::view-transition /* de overlay die de hele pagina bedekt */
::view-transition-group(name) /* animeert grootte en positie */
::view-transition-image-pair(name)
::view-transition-old(name) /* de "voor"-snapshot */
::view-transition-new(name) /* de "na"-snapshot */

Wat iedereen verrast: een snapshot is een foto, geen element

::view-transition-old is niet uw oude element kleiner gerenderd. Het is een statische afbeelding ervan. Een <video> in een transitie bevriest op één frame. Een spinner stopt. Tekst in de snapshot kan niet worden geselecteerd.

Bij 200ms maakt niets dat uit. Bij 1000ms wordt alles zichtbaar en licht kapot.

Naamgeving: het hele contract

Dezelfde naam aan beide kanten betekent dat de browser het ene in het andere beweegt en schaalt. De harde regel: een view-transition-name moet uniek in het document zijn.

css

.card-image {
view-transition-name: hero;
}
::view-transition-group(hero) {
animation-duration: 400ms;
animation-timing-function: ease-in-out;
}

React 19.2 en Next.js 16: één component

tsx

import { ViewTransition } from "react";
import Image from "next/image";
import Link from "next/link";
export function ProjectCard({ project }) {
return (
<Link href={"/projects/" + project.slug}>
<ViewTransition name={"project-" + project.slug}>
<Image src={project.cover} alt={project.title} />
</ViewTransition>
</Link>
);
}

share en default="none" moeten samen reizen. Met default="none" en geen share-prop stopt het genoemde paar stil met morphen.

Richting: de gebruiker vertellen waar hij naartoe is gegaan

tsx

<Link href="/projects/runvalis" transitionTypes={["nav-forward"]}>
Runvalis
</Link>
<ViewTransition
enter={{ "nav-forward": "slide-in", "nav-back": "slide-out", default: "none" }}
exit={{ "nav-forward": "slide-out", "nav-back": "slide-in", default: "none" }}
default="none"
>
<main>{children}</main>
</ViewTransition>

De versie zonder framework

css

@view-transition {
navigation: auto;
}

Valkuilen en best practices

  • Altijd behandelen als progressive enhancement.
  • Debuggen in slow motion. Het Animations-paneel in Chrome DevTools laat afspelen reduceren naar 25% of 10%.
  • prefers-reduced-motion respecteren.
  • De overlay eet standaard klikken. ::view-transition { pointer-events: none } instellen.
  • Snapshots kosten geheugen.
  • De header verankeren.
  • Een view transition is onzichtbaar voor ondersteunende technologie.

Conclusie

View Transitions zijn hetzelfde argument toegepast op navigatie: in plaats van FLIP met de hand uit te voeren, declareer je wat twee staten verbindt en laat je de browser de verbinding uit twee foto's tekenen.