Laravel Daily's

Scroll Restoration in Inertia 3.x: Keep Your Place Across Laravel + Vue Visits

Bright illustration of scroll restoration across Laravel, Vue, and Inertia browser visits

A smooth single-page application should remember where users were.

They might open an item from a long feed, switch between tabs, refresh data, and press Back. Each action should return them to a useful place.

Inertia 3.x handles much of this automatically. It resets scroll for ordinary visits, records scroll positions in browser history, and restores them during Back and Forward navigation.

You only need a few options when your interface has custom behavior.

Inertia’s default scroll behavior: Reset on visits, restore in history

Inertia mimics the browser’s usual navigation model.

When a user makes a normal Inertia visit, the document body scrolls to the top. This applies when navigating through an Inertia <Link> or the router.

router.visit('/projects')

If the user later presses the browser’s Back button, Inertia restores the scroll position associated with the previous history entry.

That means a common list-and-detail flow works without custom scroll code:

  1. The user scrolls through /projects.
  2. They open /projects/42.
  3. They press Back.
  4. Inertia returns them to the previous position in the project list.

Inertia tracks this position as part of its history management. The behavior works best when navigation stays inside Inertia. Full-page browser reloads and unrelated navigation paths follow the browser’s own rules.

This default is useful for most pages. New destinations begin at the top, while returning users land where they left off.

The Inertia scroll management documentation covers the underlying behavior in detail.

Illustration of default scroll reset and browser history restoration with Laravel, Vue, and Inertia

preserveScroll: Keep the current position during a visit

Some visits should not move the user.

Tabbed content is a clear example. A tab may update the page data while keeping the surrounding layout unchanged. Resetting the scroll position after every tab click makes the interface feel disconnected.

Use preserve-scroll on an Inertia link:

<Link href="/reports?tab=activity" preserve-scroll>
    Activity
</Link>

For programmatic visits, use the preserveScroll option:

router.get('/reports', { tab: 'activity' }, {
  preserveScroll: true,
})

This prevents Inertia from resetting the document scroll position after the response arrives.

Use the option for visits that update content in place. Do not apply it to every link by default. Ordinary page changes should usually start at the top, which gives users a clear sense of moving to a new destination.

You can also preserve scroll only when a response contains validation errors:

router.post('/settings', data, {
  preserveScroll: 'errors',
})

This is useful when a form sits lower on the page. A successful submission can follow the normal navigation flow. A failed submission keeps the user beside the fields that need attention.

Inertia also supports a callback when the decision depends on returned page data:

router.post('/settings', data, {
  preserveScroll: (page) => page.props.showSummary,
})

The important distinction is simple:

  • preserveScroll controls the scroll position for the current visit.
  • It does not decide whether Vue keeps the current component instance.

That second responsibility belongs to preserveState.

scroll-region: Manage nested scrollable elements

Many application layouts do not scroll through the document body.

A dashboard might keep a fixed sidebar and header while the main content panel handles scrolling. A mail client might use one scrollable message list beside another scrollable detail panel.

In these cases, Inertia cannot manage every scrollable element automatically. Mark each element that should participate in scroll management with scroll-region.

<div class="overflow-y-auto" scroll-region>
    <!-- Long list content -->
</div>

The attribute tells Inertia to treat the element as a managed scroll container.

On a normal visit, Inertia resets the region to the top. On browser Back and Forward actions, it restores the region’s previous scroll position along with the page history entry.

Without scroll-region, Inertia manages the document body but does not know which nested elements represent meaningful page scroll.

This is one of the most common causes of confusing behavior. The outer page may restore correctly while the inner feed returns to the beginning.

A custom scroll container also needs a real scrolling boundary. The element must have constrained dimensions and an overflow rule, such as overflow-y-auto. The attribute alone does not create scrolling.

Use a recent Inertia version when working with complex nested layouts. Scroll-region behavior has received fixes across earlier releases, and upgrading is often the fastest way to resolve inconsistent restoration.

Scroll regions and preserved visits

preserveScroll applies to managed scroll regions as well as the document body.

Consider a filtered list inside a panel:

<div class="max-h-screen overflow-y-auto" scroll-region>
    <Link
        href="/orders?status=paid"
        preserve-scroll
    >
        Paid
    </Link>
</div>

The visit can replace the list data without moving the panel’s scrollbar.

This works well for filters, tabs, sorting controls, and small updates that do not change the user’s broader context.

Still, preserving a position is not always correct. If a filter changes the list from thousands of records to three, the old offset may no longer be meaningful. In that case, let the visit reset the region or choose a new position deliberately.

Scroll behavior should follow the meaning of the interaction. Preserve position when the user is refining the same view. Reset when they have chosen a new destination.

Bright illustration of preserveScroll and preserveState keeping a tabbed Laravel Vue interface in place

Combine preserveScroll with preserveState

Scroll position and component state often work together.

A same-page visit can replace server-side props. It may also recreate the page component unless you ask Inertia to preserve its state. That can reset local values such as an open tab, a search input, or a temporary UI selection.

For a data refresh that should keep the user’s place, combine both options:

router.get('/orders', { status: 'paid' }, {
  preserveState: true,
  preserveScroll: true,
})

The result is more coherent:

  • The server sends updated data.
  • Vue keeps the existing page component instance.
  • Local UI state remains available.
  • The document or scroll region does not jump.

When you only need to refresh the current page, router.reload() is often the clearest choice:

router.reload()

Inertia’s reload() helper preserves both component state and scroll position by default. It is useful for refreshing notifications, dashboard metrics, or a list after an external change.

For forms, conditional preservation is often more appropriate:

router.post('/orders', data, {
  preserveState: 'errors',
  preserveScroll: 'errors',
})

The page keeps its local state and scroll position when validation fails. A successful response can follow the application’s normal redirect behavior.

URL-driven lists need stable scroll context

Scroll restoration works best when the URL describes the content the user sees.

A paginated feed is a practical example. If the current page, filter, or sort order lives in the URL, each view has a distinct history entry:

/orders?page=3&status=paid

The URL identifies the dataset. Inertia’s history state identifies the user’s position within that dataset.

Together, they make Back and Forward navigation predictable. A user can open an order from page three, return to the list, and see page three at the earlier position.

This is especially important for links that people share or bookmark. A deep link should reconstruct the correct page of results rather than relying on hidden client memory.

Laravel’s routing system provides the server-side foundation for these URLs. Named routes and query parameters keep list views addressable, while Inertia handles the client-side visit. You can review the relevant patterns in the Laravel routing documentation.

The same principle applies to feeds with filters and search terms:

/articles?topic=php&page=4

Do not treat the URL as decoration. It is part of the state model. When the URL, returned props, and scroll position describe the same view, browser history becomes a reliable navigation tool.

Illustration of a nested scroll region, paginated feed, deep link, and Back button in an Inertia SPA

A practical decision guide

Use the default behavior when navigating to a new page:

<Link href="/customers">Customers</Link>

Use preserve-scroll when a link updates the current view without changing the user’s broader context:

<Link href="/customers?segment=trial" preserve-scroll>
    Trial customers
</Link>

Use scroll-region when an element owns its own scrollbar:

<div scroll-region class="overflow-y-auto">
    <!-- Scrollable content -->
</div>

Use preserveState when local Vue state should survive a same-page visit:

router.get('/customers', filters, {
  preserveState: true,
  preserveScroll: true,
})

Use router.reload() when you want fresh server data without losing the current page state or position.

For applications built with a modern PHP web framework, this keeps the server and client responsibilities clear. Laravel handles routes and data. Vue renders the interface. Inertia connects visits, history, state, and scroll behavior without requiring a separate client-side routing system.

That is also why Inertia fits naturally alongside other PHP developer tools. The same stack can serve a web interface, power background jobs, and build a REST API with PHP when a project needs mobile or third-party clients.

Keep the user’s place meaningful

Scroll restoration is not about preserving every pixel after every action.

It is about maintaining context.

Let ordinary visits reset to the top. Let browser history restore previous positions. Mark nested containers with scroll-region. Add preserveScroll to focused, in-place updates. Pair it with preserveState when the page’s local UI should remain intact.

Most importantly, let URLs describe paginated and filtered content. When the URL and history state work together, users can move through long Laravel and Vue applications without losing their place.

Previous
Building an AI-Powered Laravel Support Chatbot with OpenAI and Laravel AI SDK
Next
Expose AI Features as a REST API: Build a Production-Ready Laravel Endpoint with the AI SDK