View Transitions API en App Router de Next.js

La View Transitions API ofrece una forma sencilla de transicionar cualquier cambio visual del DOM de un estado a otro. Esto puede incluir pequeños cambios de contenido como alternar algún contenido, o cambios más amplios como navegar de una página a otra.

Este artículo se centra en mostrar cómo puedes utilizarlo en tu aplicación web con Next.js usando App Router.

Cómo usar View Transitions API en App Router de Next.js

Next.js en su version 14.2 no soporta la View Transitions API, pero puedes implementarla tu mismo de forma muy sencilla.

Si lo quieres hacer tú mismo, puedes seguir los siguientes pasos:

  1. Utiliza un contexto de React que gestione el final de la transición y utilizarlo en el layout.tsx.
'use client'
 
import { createContext, use, useEffect, useState } from 'react'
import type { Dispatch, PropsWithChildren, SetStateAction } from 'react'
 
type ViewTransitionsContextValue = (() => void) | null
 
type ViewTransitionsContextType = Dispatch<
  SetStateAction<ViewTransitionsContextValue>
>
 
const ViewTransitionsContext = createContext<ViewTransitionsContextType>(
  () => () => {},
)
 
export function ViewTransitionsApiProvider({ children }: PropsWithChildren) {
  const [finishViewTransition, setFinishViewTransition] =
    useState<ViewTransitionsContextValue>(null)
 
  useEffect(() => {
    if (finishViewTransition) {
      finishViewTransition()
      setFinishViewTransition(null)
    }
  }, [finishViewTransition])
 
  return (
    <ViewTransitionsContext.Provider value={setFinishViewTransition}>
      {children}
    </ViewTransitionsContext.Provider>
  )
}
 
export function useSetFinishViewTransition() {
  return use(ViewTransitionsContext)
}
  1. Crea una abstracción de next/link para poder dar soporte a la transición cuando se navega.
import { startTransition } from 'react'
import NextLink from 'next/link'
import { useRouter } from 'next/navigation'
 
import { useSetFinishViewTransition } from './viewTransitionsApiContext'
 
// From https://github.com/vercel/next.js/blob/v14.2.2/packages/next/src/client/link.tsx#L180C1-L191C2
function isModifiedEvent(event: React.MouseEvent): boolean {
  const eventTarget = event.currentTarget as HTMLAnchorElement | SVGAElement
  const target = eventTarget.getAttribute('target')
  return (
    (target && target !== '_self') ||
    event.metaKey ||
    event.ctrlKey ||
    event.shiftKey ||
    event.altKey || // triggers resource download
    (event.nativeEvent && event.nativeEvent.which === 2)
  )
}
 
// From https://github.com/vercel/next.js/blob/v14.2.2/packages/next/src/client/link.tsx#L204-L217
function shouldPreserveDefault(
  e: React.MouseEvent<HTMLAnchorElement>,
): boolean {
  const { nodeName } = e.currentTarget
 
  // anchors inside an svg have a lowercase nodeName
  const isAnchorNodeName = nodeName.toUpperCase() === 'A'
 
  if (isAnchorNodeName && isModifiedEvent(e)) {
    // ignore click for browser’s default behavior
    return true
  }
 
  return false
}
 
export function NextLinkWithTransition(
  props: React.ComponentProps<typeof NextLink>,
) {
  const router = useRouter()
  const finishViewTransition = useSetFinishViewTransition()
 
  const { href, replace, scroll } = props
 
  const onClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
    if (props.onClick) {
      props.onClick(e)
    }
 
    if ('startViewTransition' in document) {
      if (shouldPreserveDefault(e)) {
        return
      }
 
      e.preventDefault()
 
      document.startViewTransition(
        () =>
          new Promise<void>((resolve) => {
            startTransition(() => {
              router[replace ? 'replace' : 'push'](href as string, {
                scroll: scroll ?? true,
              })
              finishViewTransition(() => resolve)
            })
          }),
      )
    }
  }
 
  return <NextLink {...props} onClick={onClick} />
}
  1. Define las transiciones con CSS e impórtalo en el layout.tsx.
.vt-article-title {
  view-transition-name: article-title;
}

Te dejo los enlaces a la implementación de estos pasos en mi aplicación web:

A esta implementación le falta soporte para las navegaciones del navegador, como el botón de atrás o adelante.

En caso de que prefieras delegar esta tarea a una librería, puedes utilizar el paquete next-view-transitions, que es en la que me he basado para hacer esta implementación.

Soporte en otros frameworks o librerías

Aprende más sobre View Transitions API

Si quieres profundizar más en la View Transitions API, te recomiendo algunos recursos: