Consistente overgangen tussen documenten

Het probleem

Weergaveovergangen tussen documenten animeren elementen tussen 2 pagina's tijdens een navigatie met dezelfde oorsprong. De browser maakt een momentopname van de oude pagina, navigeert en animeert dan van de momentopname naar de nieuwe pagina. Als de nieuwe pagina nog niet klaar is met het laden van essentiële bronnen (stylesheets, opmaakscripts of belangrijke DOM-elementen), wordt de overgang geanimeerd naar een onvolledige of niet-opgemaakte status. Dit veroorzaakt visuele problemen, zoals elementen die naar de verkeerde posities veranderen, content die halverwege de animatie opnieuw wordt ingedeeld of reservelettertypen die naar weblettertypen flitsen nadat de overgang is voltooid.

De oplossing

Gebruik blocking="render" voor essentiële <link>- en <script>-elementen in de <head> van de nieuwe pagina en gebruik <link rel="expect"> om de weergave te blokkeren totdat specifieke DOM-elementen zijn geparseerd. Dit zorgt ervoor dat de browser de animatie voor de weergaveovergang pas start als de visuele status van de nieuwe pagina stabiel is. De browser blijft de HTML op de achtergrond parseren. Alleen het tekenen wordt uitgesteld.

Implementatiestrategie

  1. VERPLICHT: Meld je aan voor weergaveovergangen tussen documenten met de CSS-at-regel @view-transition op beide pagina's.
  2. VERPLICHT: Zorg dat essentiële stylesheets in de <head> staan. Stylesheets in de <head> blokkeren standaard de weergave. Dynamisch geïnjecteerde stylesheets vereisen expliciete blocking="render".
  3. Gebruik WEL blocking="render" voor <script>-elementen die moeten worden uitgevoerd voordat de overgang wordt geanimeerd (bijvoorbeeld scripts die een thema toepassen of de vormgeving beïnvloeden).
  4. Gebruik WEL <link rel="expect" href="#element-id" blocking="render"> om de weergave te blokkeren totdat de content boven de vouw is geparseerd. Dit geldt voor alle overgangstypen: crossfades op de hele pagina (om animaties naar een lege pagina te voorkomen), morph-animaties (om te zorgen dat benoemde elementen in de DOM bestaan) en scriptafhankelijke lay-outs (om te zorgen dat gestileerde content wordt geparseerd).
  5. Blokkeer de weergave van niet-essentiële content NIET. Blokkeer alleen bronnen en elementen die van invloed zijn op de eerste viewport. Als je te veel content blokkeert, wordt de overgang vertraagd en worden de waargenomen prestaties slechter.

Implementatiehandleiding

Stap 1: Meld je aan voor weergaveovergangen tussen documenten

VERPLICHT: Zowel de bron- als de bestemmingspagina moet de at-regel @view-transition bevatten. Zonder dit vindt er geen overgang tussen documenten plaats.

/*
  MANDATORY: Include this rule in every page that participates
  in cross-document view transitions.
  `navigation: auto` enables transitions for standard navigations
  (link clicks, form submissions, back/forward).
*/
@view-transition {
  navigation: auto;
}

/* MANDATORY Copy-Paste Safety: Disable cross-document view transitions for users requesting reduced motion */
@media (prefers-reduced-motion: reduce) {
  @view-transition {
    navigation: none;
  }
}

Stap 2: Blokkeer de weergave totdat kritieke scripts zijn uitgevoerd

Als een niet-blokkerend script in de <head> moet worden uitgevoerd voordat de overgang wordt geanimeerd (bijvoorbeeld om een themaklasse toe te passen of de vormgeving te beïnvloeden), markeer je het met blocking="render". Zonder deze instelling kunnen async-, defer- of type="module"-scripts worden uitgevoerd nadat de overstap al is gestart.

<head>
  <!--
    DO: Mark layout-critical scripts with blocking="render".
  -->
  <script type=module blocking="render">
    // Example: apply a stored theme before the page renders,
    // so the transition snapshot reflects the correct theme.
    document.documentElement.dataset.theme =
      localStorage.getItem('theme') || 'light';
  </script>
</head>

Stap 3: Blokkeer de weergave totdat belangrijke DOM-elementen zijn geparseerd

Stylesheets en blocking="render"-scripts in de <head> garanderen alleen dat de <head> volledig is verwerkt. Ze wachten niet tot <body>-content is geparseerd. Zonder aanvullende blokkering kan de browser de momentopname van de nieuwe pagina maken voordat de elementen boven de vouw in de DOM bestaan. Dit leidt tot een overgang die animeert naar een lege of gedeeltelijk weergegeven pagina.

<link rel="expect"> lost dit op door de weergave te blokkeren totdat een specifiek element (geïdentificeerd door de id) is geparseerd. De waarde href moet een fragment-ID zijn (bijv. #hero) die overeenkomt met het kenmerk id van het doelelement. Zodra de afsluitende tag van dat element is geparseerd, wordt het weergaveblok vrijgegeven. <link rel="expect"> garandeert alleen dat het doel-DOM-element in het document bestaat. Er wordt niet gewacht tot externe subbronnen (zoals <img>-pixels) zijn gedownload of gedecodeerd.

Gebruik WEL <link rel="expect"> in de volgende scenario's:

Use case 1: Cross-fade op volledige pagina

Zelfs als geen van de afzonderlijke elementen een view-transition-name heeft, wordt de hele pagina met de standaardovergang root overvloeid. Als de momentopname van de nieuwe pagina wordt gemaakt voordat de content boven de vouw is geparseerd, wordt de crossfade geanimeerd van de oude pagina naar een lege of onvolledige pagina. Blokkeer weergave van een element dat het einde van de zichtbare content boven de vouw markeert.

<head>
  <link rel="stylesheet" href="/css/styles.css">

  <!--
    DO: Block rendering until the main content area is parsed,
    even for a simple cross-fade. Without this, the browser may
    snapshot the page before visible content exists in the DOM,
    causing the cross-fade to reveal a blank or partial page.
  -->
  <link rel="expect" href="#main-content" blocking="render">
</head>
<body>
  <header>...</header>
  <main id="main-content">
    <h1>Page Title</h1>
    <p>Above-the-fold content the user should see immediately.</p>
  </main>
  <!-- Content below the fold does NOT need to be blocked on -->
  <section>...</section>
</body>

Use case 2: Morph-animaties tussen specifieke elementen

Als elementen op beide pagina's een view-transition-name delen, verandert de browser ze soepel tijdens de navigatie. Als het doelelement niet is geparseerd wanneer de overgang begint, kan de browser het niet vinden. De morph wordt dan gedegradeerd tot afzonderlijke animaties voor afsluiten en invoeren. Blokkeer de weergave totdat het element met de view-transition-name is geparseerd.

Omdat <link rel="expect"> de weergave alleen onderbreekt totdat het doel-DOM-element is geparseerd en niet wacht tot afbeeldingsgegevens zijn gedownload of gedecodeerd, kan een niet-gecachte <img> worden vastgelegd als een lege rechthoek in de momentopname van de nieuwe pagina. Als je lege snapshots of verschuivingen in de vormgeving tijdens beeldovergangen wilt voorkomen, laad je essentiële overgangsafbeeldingen vooraf of decodeer je ze vooraf (bijvoorbeeld met <link rel="preload" as="image"> of door de bestemmingspagina vooraf te renderen) en bied je expliciete alternatieven voor de grootte (kenmerken width en height of CSS aspect-ratio, plus een tijdelijke achtergrondkleur).

<head>
  <link rel="stylesheet" href="/css/styles.css">

  <!--
    DO: Preload critical transition images so their pixels are
    available to decode before the new-page snapshot is captured.
    <link rel="expect"> only waits for the DOM element to be parsed,
    NOT for image pixels to download or decode.
  -->
  <link rel="preload" as="image" href="/img/product.webp" fetchpriority="high">

  <!--
    DO: Block rendering until the element participating in the
    morph animation has been parsed. Without this, the browser
    may start the transition before #hero exists, causing the
    morph to degrade to a fade-out/fade-in.
  -->
  <link rel="expect" href="#hero" blocking="render">

  <!--
    When multiple blocking="render" resources are present,
    rendering is blocked until ALL of them are satisfied.
    Here, the browser waits for both the script to execute
    AND the #hero element to be parsed — whichever comes last.
  -->
  <script async blocking="render" src="/js/transition-setup.js"></script>
</head>
<body>
  <header>...</header>
  <section id="hero">
    <h1 style="view-transition-name: page-title">Product Name</h1>
    <!--
      DO: Provide explicit dimensions (width/height or aspect-ratio)
      as a sizing fallback so the snapshot captures the correct layout
      box even if image decoding is still in progress.
    -->
    <img
      style="view-transition-name: hero-image"
      src="/img/product.webp"
      width="800"
      height="600"
      alt="Product"
    >
  </section>
</body>

Stap 4: Gebruik mediaquery's voor responsieve weergaveblokkering

Verschillende formaten van het weergavegebied kunnen verschillende hoeveelheden content boven de vouw tonen. Gebruik het kenmerk media op <link rel="expect"> om de weergave alleen te blokkeren voor de content die zichtbaar is bij een bepaalde breedte van het weergavegebied.

<head>
  <!--
    DO: Use media queries to conditionally block rendering.
    On wide screens, both the hero and the sidebar are visible,
    so block until both are parsed. On narrow screens, only the
    hero is visible initially.
  -->
  <link
    rel="expect"
    href="#hero"
    blocking="render"
    media="screen and (width <= 768px)"
  >
  <link
    rel="expect"
    href="#sidebar"
    blocking="render"
    media="screen and (width > 768px)"
  >
</head>

Stap 5: Gebruik pagereveal voor contextafhankelijke overgangen (optioneel)

De gebeurtenis pagereveal is niet vereist voor de kernstrategie voor weergaveblokkering. Dit is alleen nodig als view-transition-name-waarden dynamisch moeten worden toegewezen op basis van waar de gebruiker vandaan is genavigeerd, bijvoorbeeld als een specifiek lijstitem wordt omgezet in een kop op een detailpagina.

Als view-transition-name-waarden statisch worden toegewezen in CSS of als u alleen de standaard crossfade voor de hele pagina gebruikt, slaat u deze stap helemaal over.

<head>
  <!--
    MANDATORY: The pagereveal listener must be registered before
    the page renders. Inline small setup scripts with blocking="render"
    to avoid an extra network round-trip on cold loads, or use an
    external async script with blocking="render" so the listener is
    registered early without blocking HTML parsing.
    If the listener is registered too late (e.g., in a deferred
    script), the event may have already fired.
  -->
  <script async blocking="render" src="/js/transition-setup.js"></script>
</head>
// transition-setup.js
window.addEventListener('pagereveal', async (event) => {
  if (!event.viewTransition) return;

  const from = navigation.activation?.from;
  if (!from) return;

  const fromUrl = new URL(from.url);

  // DO: Assign view-transition-name based on navigation context.
  // This enables a morph animation from the product card on the
  // list page to the heading on the detail page.
  if (fromUrl.pathname === '/products/') {
    const heading = document.querySelector('main h1');
    if (!heading) return;

    heading.style.viewTransitionName = 'product-title';

    try {
      await event.viewTransition.ready;
    } catch (error) {
      // Skipped view transitions reject `ready` with InvalidStateError.
      if (error.name !== 'InvalidStateError') {
        throw error;
      }
    } finally {
      // MANDATORY: Remove the temporary name after the transition
      // finishes. Stale names interfere with subsequent navigations
      // and prevent the page from entering the bfcache.
      await event.viewTransition.finished;
      heading.style.viewTransitionName = '';
    }
  }
});

Praktische tips

  • Wijs view-transition-name waar mogelijk toe via CSS. Reserveer JavaScript-toewijzing (via pagereveal) voor gevallen waarin de naam afhankelijk is van de navigatiecontext.
  • DO: Houd scripts die de weergave blokkeren klein en snel. De browser heeft een ingebouwde time-out (ongeveer 4 seconden), waarna de overgang helemaal wordt overgeslagen (viewTransition.ready wordt afgewezen met een InvalidStateError als er een overgang is gestart).
  • Gebruik <link rel="expect"> NIET om elementen diep op de pagina te blokkeren die niet zichtbaar zijn in de eerste viewport. Dit vertraagt de overgang zonder visueel voordeel.
  • Markeer GEEN loaders voor analyse, advertentienetwerken, tagbeheer of andere loaders van derden met blocking="render", zelfs niet als ze content injecteren in de eerste viewport. De netwerklatentie telt mee voor de time-out van de overgang en de uitvoer maakt geen deel uit van de stabiele status van de pagina.
  • Wijs NIET dezelfde view-transition-name toe aan meerdere elementen op dezelfde pagina. Als er dubbele namen zijn, wordt de hele overgang overgeslagen.
  • Impact van timing van ondersteunende technologie: Als je blocking="render" gebruikt, worden visuele updates en de eerste weergave vertraagd. Dit voorkomt visuele fouten voor gebruikers die kunnen zien, maar kan leiden tot verwerkingslatentie of uitgestelde initialisatie voor schermlezers en andere ondersteunende technologieën die afhankelijk zijn van gerenderde toegankelijkheidsbomen. Weeg de voordelen van visuele continuïteit af tegen de initiële leeslatentie voor niet-visuele gebruikers en zorg dat scripts die de weergave blokkeren, minimaal en uiterst geoptimaliseerd zijn.

Reservestrategieën

Weergaveovergangen tussen documenten zijn een uitstekende kandidaat voor progressive enhancement. In browsers die deze niet ondersteunen, wordt de regel @view-transition genegeerd en vinden standaardnavigaties van dezelfde oorsprong plaats, net zoals zonder de functie. Browsers die dit ondersteunen, krijgen soepele overgangen. Alle andere browsers krijgen standaardnavigatie. Beperkte browserondersteuning is geen reden om de functie niet te gebruiken.

Alle browsers die weergaveovergangen tussen documenten ondersteunen, ondersteunen ook blocking="render" en <link rel="expect">. Er is dus geen afzonderlijke fallback nodig voor de weergaveblokkerende functies die in deze handleiding worden beschreven.

Andere overwegingen

  1. Prestatie-impact: Elke bron die de weergave blokkeert, vertraagt de start van de animatie voor de weergaveovergang. Beperk het aantal scripts dat de weergave blokkeert en gebruik <link rel="expect"> alleen voor elementen die boven de vouw staan. Laat bestemmingspagina's vooraf renderen met de Speculation Rules API om laadvertragingen helemaal te elimineren.
  2. Time-outgedrag: Als de gecombineerde weergaveblokkerende tijd langer is dan ongeveer 4 seconden, slaat de browser de overgang over (en wordt elke actieve viewTransition.ready-promise afgewezen met een InvalidStateError). Zorg dat essentiële bronnen ruim binnen deze periode worden geladen.
  3. Compatibiliteit met bfcache: Tijdelijke view-transition-name-toewijzingen die na de overgang niet worden opgeschoond, kunnen voorkomen dat de pagina de bfcache binnengaat. Verwijder dynamisch toegewezen namen altijd in de callback finished.