livewire/livewire: wire:navigate view transitions backported to 4.x
Contributed by calebporzio
Part of the livewire v4.4.7 release
Pull request #10678 backports #10622 from main onto the 4.x branch, adding native browser View Transitions to wire:navigate page visits. Any page can now opt in with wire:transition.navigate — either on a single content region or on <html> for a full-page transition — and individually named elements can morph from one page into the next. The feature is entirely opt-in: pages without the new directive keep swapping instantly, exactly as before, so upgrading brings no behavioral change, only new capability plus substantially expanded documentation.
A new .navigate modifier for wire:transition
Until now, wire:transition only animated elements during Livewire component updates. The new .navigate modifier reuses the same syntax for wire:navigate page visits instead. Mark the part of your layout that changes between pages:
<main wire:transition.navigate>
{{ $slot }}
</main>
Now a navigation between pages that both mark a region runs a real browser transition rather than an instant swap. A transition triggers when either the outgoing or the incoming page carries the directive, so clicked links, programmatic Livewire.navigate() calls, and back/forward restores all behave consistently. An unnamed directive marks the page's primary transition region — one per page — while additional regions take unique names, like wire:transition.navigate="secondary".
Element-scoped regions keep your shell stationary
Because the directive is element-scoped, persistent UI needs no special CSS. Mark the changing <main> on both pages and leave the sidebar and header unmarked; they stay visually stationary while the content animates. Livewire handles the mechanics: during element-scoped transitions it temporarily disables the browser's root snapshot animation with an injected stylesheet so the unmarked shell doesn't fade along with the page, then removes the stylesheet when the transition finishes.
If you prefer the browser's native full-page transition instead, place the directive on the document element in your application layout — the root then keeps its native animation:
<html wire:transition.navigate>
Placing it on <body> is a middle ground: the entire body transitions as a single element region.
Custom animations and morphing shared elements
The unnamed region uses the transition name livewire-navigate and the navigate transition type, keeping your rules separate from ordinary wire:transition component animations. Override the default with typed selectors in your shared resources/css/app.css — the stylesheet your layout loads via @vite, so it exists on both sides of every navigation:
html:active-view-transition-type(navigate)::view-transition-old(livewire-navigate) {
animation: navigate-out 150ms ease-in both;
}
To morph an element between pages, give matching elements the same name; the browser removes them from their parent's snapshot and transitions them independently, even across different tags:
<!-- /posts -->
<h2 wire:transition.navigate="post-{{ $post->id }}">{{ $post->title }}</h2>
<!-- /posts/1 -->
<h1 wire:transition.navigate="post-{{ $post->id }}">{{ $post->title }}</h1>
Names are assigned immediately before the transition and cleared afterwards, so they never leave permanent stacking contexts behind. Like id attributes, they must be unique per page — include an identifier when rendering inside loops, and note that Livewire logs a console warning if a page declares a second unnamed region.
Fallbacks and guards
Navigation falls back to an instant swap when View Transitions aren't supported, when the user prefers reduced motion, or when a modal dialog or popover is open — transition snapshots would otherwise paint above the top layer. If a dialog opens mid-transition, the running transition is skipped. Firefox supports the API but not typed transitions, so it plays the browser's default animation unless you also ship untyped fallback selectors.
Nothing about existing behavior changes: plain wire:transition still scopes to component updates, is never named or animated by page visits, and applications without the directive never invoke startViewTransition at all — the backport includes a browser test asserting exactly that.
Upgrade impact
For someone coming from v3.8.10, this release is purely additive: no attribute changes meaning, no markup must be updated, and wire:navigate swaps identically until you opt in. The cherry-pick applied cleanly to 4.x, including documentation and browser tests; the JavaScript suite passed (110 tests), the SupportNavigate and SupportTransitions browser suites passed (77 tests, 443 assertions), and a manual Chrome demo confirmed a stationary sidebar, working back-button restoration, and an active content transition.
To adopt, mark a region or <html>, put any transition CSS in the shared stylesheet loaded by @vite, and enhance pages progressively — links, Livewire.navigate(), and history navigation all pick it up automatically.
Takeaways
wire:transition.navigateoptswire:navigatevisits into native View Transitions: unnamed for a primary region, named for extra regions or morphing shared elements.<html>opts into the browser's full-page transition; element regions keep unmarked shells like sidebars stationary.- Transitions skip automatically for unsupported browsers, reduced-motion users, and open dialogs or popovers; Firefox uses default animations.
- Purely opt-in with zero default behavior change, so upgrading is safe and adoption is per-page.