Laravel Daily's

View Transitions in Inertia 3.x: Silky Page Animations for Your Laravel + Vue SPA

Illustrative Laravel and Vue page transition with colorful browser panels and motion trails

Inertia 3.x adds first-class support for the browser’s View Transitions API. You can animate navigation between Laravel and Vue pages without building a custom transition system.

The result is a smoother SPA experience with a small amount of configuration. You can enable the default cross-fade, inspect transition lifecycle promises, customize the animation with CSS, or animate individual elements between pages.

This tutorial walks through each option in a practical Laravel + Vue application.

How Inertia view transitions work

Inertia handles navigation between server-rendered page responses and client-side Vue components. Laravel still returns an Inertia response from a controller, while the Vue adapter updates the page in the browser.

View transitions sit around that page update. The browser captures the outgoing view, receives the new DOM state, and animates between the two snapshots.

Inertia’s viewTransition option accepts either:

  • true, which enables the default cross-fade.
  • A callback that receives the browser’s ViewTransition instance.

In browsers without View Transition API support, Inertia falls back to its standard page transition behavior. Your Laravel application does not need special server-side configuration.

That separation is useful whether you are building a dashboard, a customer portal, or a product that uses Laravel as a PHP web framework with a modern Vue frontend.

Start with a Laravel Inertia response

The Laravel side remains familiar. Your controller can return an Inertia page as usual:

<?php

namespace App\Http\Controllers;

use Inertia\Inertia;
use Inertia\Response;

class DashboardController extends Controller
{
    public function __invoke(): Response
    {
        return Inertia::render('Dashboard', [
            'user' => auth()->user(),
        ]);
    }
}

Register the route:

use App\Http\Controllers\DashboardController;
use Illuminate\Support\Facades\Route;

Route::get('/dashboard', DashboardController::class)
    ->middleware('auth')
    ->name('dashboard');

The transition is configured in your client-side Inertia code. Laravel continues to provide routing, authentication, data loading, validation, and responses.

This is one reason Inertia works well alongside Laravel’s broader ecosystem. You can use the framework for application logic while Vue handles rich interactions in the browser. The same Laravel foundation can also support teams that want to build a REST API with PHP for mobile clients or separate frontends.

Enable a transition for one visit

For programmatic navigation, pass viewTransition: true to router.visit().

<script setup>
import { router } from '@inertiajs/vue3'

function openSettings() {
    router.visit('/settings', {
        viewTransition: true,
    })
}
</script>

<template>
    <button type="button" @click="openSettings">
        Open settings
    </button>
</template>

Inertia uses the browser’s default view transition animation. By default, that is a cross-fade between the current page and the next page.

You can use the same option with shortcut methods such as router.get():

router.get('/projects', {}, {
    viewTransition: true,
})

This is a good starting point when you want to improve navigation without introducing a large animation layer. Add it to a few high-value routes first, then evaluate how the motion feels across the application.

Use transition callbacks

Passing a callback gives you access to the browser’s ViewTransition object.

import { router } from '@inertiajs/vue3'

router.visit('/settings', {
    viewTransition: (transition) => {
        transition.ready.then(() => {
            console.log('Transition ready')
        })

        transition.updateCallbackDone.then(() => {
            console.log('DOM updated')
        })

        transition.finished.then(() => {
            console.log('Transition finished')
        })
    },
})

These promises represent different points in the transition lifecycle:

  • ready resolves when the transition is prepared to animate.
  • updateCallbackDone resolves after Inertia has updated the page DOM.
  • finished resolves after the visual animation completes.

Use these hooks for transition-specific coordination. For example, you might pause an unrelated animation before navigation, update a page-level loading indicator after the DOM changes, or clean up temporary state after the transition finishes.

Laravel and Vue illustration showing view transition lifecycle checkpoints

Avoid putting essential application behavior inside finished. A browser may skip or cancel a transition, and unsupported browsers use the fallback behavior. Keep business logic tied to Inertia’s visit callbacks, and use the ViewTransition promises for visual concerns.

Add transitions to Vue Link components

The Link component supports the same option.

<script setup>
import { Link } from '@inertiajs/vue3'
</script>

<template>
    <Link href="/projects" view-transition>
        View projects
    </Link>
</template>

Vue maps the view-transition attribute to the viewTransition prop. This enables the default transition for that link only.

You can also pass a callback with Vue’s binding syntax:

<script setup>
import { Link } from '@inertiajs/vue3'

function handleTransition(transition) {
    transition.finished.then(() => {
        console.log('Navigation animation complete')
    })
}
</script>

<template>
    <Link
        href="/projects"
        :view-transition="handleTransition"
    >
        View projects
    </Link>
</template>

This approach keeps the behavior close to the navigation element. It works well when only a few links need motion or when different parts of an application use different transition rules.

Configure transitions globally

If you want every Inertia visit to use a view transition, configure the visitOptions callback while initializing the app.

import { createInertiaApp } from '@inertiajs/vue3'

createInertiaApp({
    // resolve, setup, and other options...

    defaults: {
        visitOptions: (href, options) => {
            return {
                viewTransition: true,
            }
        },
    },
})

The callback receives the destination URL and the current visit options. You can also enable transitions conditionally:

createInertiaApp({
    defaults: {
        visitOptions: (href, options) => {
            if (href.startsWith('/admin')) {
                return {
                    viewTransition: true,
                }
            }

            return {}
        },
    },
})

Global configuration reduces repetition, but it also applies motion broadly. Review form submissions, destructive actions, large data views, and frequently refreshed pages before enabling transitions everywhere.

For teams comparing PHP developer tools, this is a useful Inertia pattern: the framework stays responsible for navigation and server responses, while a small client-side default controls the presentation layer.

Customize the page animation with CSS

The View Transition API exposes pseudo-elements for the old and new page snapshots. Inertia’s documentation uses ::view-transition-old(root) and ::view-transition-new(root) for page-level animations.

Add the following rules to a global stylesheet loaded by your Vue application:

@keyframes fade-in {
    from {
        opacity: 0;
    }
}

@keyframes fade-out {
    to {
        opacity: 0;
    }
}

@keyframes slide-from-right {
    from {
        transform: translateX(30px);
    }
}

@keyframes slide-to-left {
    to {
        transform: translateX(-30px);
    }
}

::view-transition-old(root) {
    animation:
        90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
        300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
    animation:
        210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
        300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

The old page fades and moves slightly left. The new page fades in from the right with a short delay.

Bright technical illustration of CSS keyframes customizing a Laravel and Vue page transition

Keep page transitions subtle. Large movement can make navigation feel slower, especially on smaller screens. A short fade or a small horizontal shift usually gives users enough context without competing with the content.

You can also respect reduced-motion preferences:

@media (prefers-reduced-motion: reduce) {
    ::view-transition-old(root),
    ::view-transition-new(root) {
        animation: none;
    }
}

The exact CSS support for View Transitions depends on the browser. Test the experience in your supported browser matrix and confirm that the fallback remains usable.

Animate individual elements between pages

Page-level animations are useful, but shared elements can create a stronger sense of continuity.

The view-transition-name property tells the browser to track an element between the old and new pages. The element needs the same unique name in both views.

For example, a profile page can display a large avatar:

<!-- resources/js/Pages/Profile.vue -->
<template>
    <section>
        <img
            src="/images/avatar.jpg"
            alt="User"
            class="avatar-large"
        />

        <h1>Alex Morgan</h1>
    </section>
</template>

<style>
.avatar-large {
    view-transition-name: user-avatar;
    width: auto;
    height: 200px;
    border-radius: 9999px;
}
</style>

The dashboard can use the same avatar at a smaller size:

<!-- resources/js/Pages/Dashboard.vue -->
<template>
    <header class="dashboard-header">
        <img
            src="/images/avatar.jpg"
            alt="User"
            class="avatar-small"
        />
    </header>
</template>

<style>
.avatar-small {
    view-transition-name: user-avatar;
    width: auto;
    height: 40px;
    border-radius: 9999px;
}
</style>

When navigating from Profile to Dashboard with view transitions enabled, the browser can animate the avatar from its large profile position into the smaller dashboard position.

Illustration of a large profile avatar transforming into a small dashboard avatar

Use unique names for elements that should participate in the same transition. Avoid assigning the same view-transition-name to multiple elements in one view unless your browser support and design specifically account for that behavior.

This pattern also works for product images, article thumbnails, logos, and selected navigation elements. It is most effective when the element represents the same object on both pages.

Practical implementation checklist

Before shipping view transitions in a Laravel and Vue SPA, check the following:

  1. Start with viewTransition: true on one or two links.
  2. Add a global CSS transition only after testing the default cross-fade.
  3. Use callbacks for visual coordination, not core business logic.
  4. Keep view-transition-name values unique and descriptive.
  5. Test direct loads and unsupported browsers.
  6. Respect prefers-reduced-motion.
  7. Check focus, scroll position, loading states, and form validation.
  8. Keep transitions short enough that navigation still feels immediate.

The Inertia 3.x view transition documentation covers the complete API. The manual visits documentation lists the other visit options available alongside viewTransition.

Laravel remains the server-side source of truth. Vue remains responsible for the interface. Inertia connects them with a small, expressive API that lets you add motion when it supports the user’s sense of place.

For more Laravel development resources, see the Laravel documentation and explore the framework’s ecosystem. A well-timed transition should not call attention to itself. It should simply make the next page feel like a natural continuation of the last one.

Previous
TypeScript in Your Laravel + Vue + Inertia App: Typed Props from Route to Component
Next
Human-in-the-Loop AI Agents in Laravel: Approving Tool Calls Before They Run