You're viewing the Inertia.js v2.0 documentation. Upgrade guide →
Inertia's infinite scroll feature loads additional pages of content as users scroll, replacing traditional pagination controls. This is great for applications like chat interfaces, social feeds, photo grids, and product listings.
To configure your paginated data for infinite scrolling, you should use the Inertia::scroll()
method when returning your response. This method automatically configures the proper merge behavior and normalizes pagination metadata for the frontend component.
Route::get('/users', function () {
return Inertia::render('Users/Index', [
'users' => Inertia::scroll(fn () => User::paginate())
]);
});
The Inertia::scroll()
method works with Laravel's paginate()
, simplePaginate()
, and cursorPaginate()
methods, as well as pagination data wrapped in Eloquent API resources. For more details, check out the Inertia::scroll() method documentation.
On the client side, Inertia provides the <InfiniteScroll>
component to automatically load additional pages of content. The component accepts a data
prop that specifies the key of the prop containing your paginated data. The <InfiniteScroll>
component should wrap the content that depends on the paginated data.
<InfiniteScroll data="users">
<div v-for="user in users.data" :key="user.id">
{{ user.name }}
</div>
</InfiniteScroll>
The component uses intersection observers to detect when users scroll near the end of the content and automatically triggers requests to load the next page. New data is merged with existing content rather than replacing it.
You can control how early content begins loading by setting a buffer distance. The buffer specifies how many pixels before the end of the content loading should begin.
<InfiniteScroll data="users" :buffer="500">
<!-- ... -->
</InfiniteScroll>
In the example above, content will start loading 500 pixels before reaching the end of the current content. A larger buffer loads content earlier but potentially loads content that users may never see.
The infinite scroll component updates the browser URL's query string (?page=...
) as users scroll through content. The URL reflects which page has the most visible items on screen, updating in both directions as users scroll up or down. This allows users to bookmark or share links to specific pages. You can disable this behavior to maintain the original page URL.
<InfiniteScroll data="users" preserve-url>
<!-- ... -->
</InfiniteScroll>
This is useful when infinite scroll is used for secondary content that shouldn't affect the main page URL, such as comments on a blog post or related products on a product page.
The infinite scroll component loads content in both directions when you scroll near the start or end. You can control this behavior using the only-next
and only-previous
props.
<!-- Only load the next page -->
<InfiniteScroll data="users" only-next>
<!-- ... -->
</InfiniteScroll>
<!-- Only load the previous page -->
<InfiniteScroll data="messages" only-previous>
<!-- ... -->
</InfiniteScroll>
<!-- Load in both directions (default) -->
<InfiniteScroll data="posts">
<!-- ... -->
</InfiniteScroll>
The default option is particularly useful when users start on a middle page and need to scroll in both directions to access all content.
For chat applications, timelines, or interfaces where content is sorted descendingly (newest items at the bottom), you can enable reverse mode. This configures the component to load older content when scrolling upward.
<InfiniteScroll data="messages" reverse>
<!-- ... -->
</InfiniteScroll>
In reverse mode, the component flips the loading directions so that scrolling up loads the next page (older content) and scrolling down loads the previous page (newer content). The component handles the loading positioning, but you are responsible for reversing your content to display in the correct order.
Reverse mode also enables automatic scrolling to the bottom on initial load, which you can disable with :auto-scroll="false"
.
<InfiniteScroll data="messages" reverse :auto-scroll="false">
<!-- ... -->
</InfiniteScroll>
Manual mode disables automatic loading when scrolling and allows you to control when content loads through the next
and previous
slots. For more details about available slot properties and customization options, see the Slots documentation.
<InfiniteScroll data="users" manual>
<template #previous="{ loading, fetch, hasMore }">
<button v-if="hasMore" @click="fetch" :disabled="loading">
{{ loading ? 'Loading...' : 'Load previous' }}
</button>
</template>
<!-- Your content -->
<template #next="{ loading, fetch, hasMore }">
<button v-if="hasMore" @click="fetch" :disabled="loading">
{{ loading ? 'Loading...' : 'Load more' }}
</button>
</template>
</InfiniteScroll>
You can also configure the component to automatically switch to manual mode after a certain number of pages using the manualAfter
prop.
<InfiniteScroll data="users" :manual-after="3">
<!-- ... -->
</InfiniteScroll>
The infinite scroll component provides several slots to customize the loading experience. These slots allow you to display custom loading indicators and create manual load controls. Each slot receives properties that provide loading state information and functions to trigger content loading.
The main content area where you render your data items. This slot receives loading state information.
<InfiniteScroll data="users" #default="{ loading, loadingPrevious, loadingNext }">
<!-- Your content with access to loading states -->
</InfiniteScroll>
The loading slot is used as a fallback when loading content and no custom before
or after
slots are provided. This creates a default loading indicator.
<InfiniteScroll data="users">
<!-- Your content -->
<template #loading>
Loading more users...
</template>
</InfiniteScroll>
The previous
and next
slots are rendered above and below the main content, typically used for manual load controls. These slots receive several properties including loading states, fetch functions, and mode indicators.
<InfiniteScroll data="users" :manual-after="3">
<template #previous="{ loading, fetch, hasMore, manualMode }">
<button v-if="manualMode && hasMore" @click="fetch" :disabled="loading">
{{ loading ? 'Loading...' : 'Load previous' }}
</button>
</template>
<!-- Your content -->
<template #next="{ loading, fetch, hasMore, manualMode }">
<button v-if="manualMode && hasMore" @click="fetch" :disabled="loading">
{{ loading ? 'Loading...' : 'Load more' }}
</button>
</template>
</InfiniteScroll>
The loading
, previous
, and next
slots receive the following properties:
loading
- Whether the slot is currently loading contentloadingPrevious
- Whether previous content is loadingloadingNext
- Whether next content is loadingfetch
- Function to trigger loading for the slothasMore
- Whether more content is available for the slothasPrevious
- Whether more previous content is availablehasNext
- Whether more next content is availablemanualMode
- Whether manual mode is activeautoMode
- Whether automatic loading is activeThe InfiniteScroll
component renders as a <div>
element. You may customize this to use any HTML element using the as
prop.
<InfiniteScroll data="products" as="ul">
<li v-for="product in products.data" :key="product.id">
{{ product.name }}
</li>
</InfiniteScroll>
The infinite scroll component automatically tracks content and assigns page numbers to elements for URL synchronization. When your data items are not direct children of the component's root element, you need to specify which element contains the actual data items using the itemsElement
prop.
<InfiniteScroll data="users" items-element="#table-body">
<table>
<thead>
<tr><th>Name</th></tr>
</thead>
<tbody id="table-body">
<tr v-for="user in users.data" :key="user.id">
<td>{{ user.name }}</td>
</tr>
</tbody>
</table>
</InfiniteScroll>
In this example, the component monitors the #table-body
element and automatically tags each <tr>
with a page number as new content loads. This enables proper URL updates based on which page's content is most visible in the viewport.
You can also specify custom trigger elements for loading more content using CSS selectors. This prevents the default trigger elements from being rendered and uses intersection observers on your custom elements instead.
<InfiniteScroll
data="users"
items-element="#table-body"
start-element="#table-header"
end-element="#table-footer"
>
<table>
<thead id="table-header">
<tr><th>Name</th></tr>
</thead>
<tbody id="table-body">
<tr v-for="user in users.data" :key="user.id">
<td>{{ user.name }}</td>
</tr>
</tbody>
<tfoot id="table-footer">
<tr><td>Footer</td></tr>
</tfoot>
</table>
</InfiniteScroll>
Alternatively, you can use template refs instead of CSS selectors. This avoids adding HTML attributes and provides direct element references.
<script setup>
import { ref } from 'vue'
const tableHeader = ref()
const tableFooter = ref()
const tableBody = ref()
</script>
<template>
<InfiniteScroll
data="users"
:items-element="() => tableBody"
:start-element="() => tableHeader"
:end-element="() => tableFooter"
>
<table>
<thead ref="tableHeader">
<tr><th>Name</th></tr>
</thead>
<tbody ref="tableBody">
<tr v-for="user in users.data" :key="user.id">
<td>{{ user.name }}</td>
</tr>
</tbody>
<tfoot ref="tableFooter">
<tr><td>Footer</td></tr>
</tfoot>
</table>
</InfiniteScroll>
</template>
The infinite scroll component works within any scrollable container, not just the main document. The component automatically adapts to use the custom scroll container for trigger detection and calculations instead of the main document scroll.
<div style="height: 400px; overflow-y: auto;">
<InfiniteScroll data="users">
<div v-for="user in users.data" :key="user.id">
{{ user.name }}
</div>
</InfiniteScroll>
</div>
When you need to trigger loading actions programmatically, you may use a template ref.
<script setup>
import { ref } from 'vue'
const infiniteScrollRef = ref(null)
const fetchNext = () => {
infiniteScrollRef.value?.fetchNext()
}
</script>
<template>
<button @click="fetchNext">Load More</button>
<InfiniteScroll ref="infiniteScrollRef" data="users" manual>
<!-- Your content -->
</InfiniteScroll>
</template>
The component exposes the following methods:
fetchNext()
- Manually fetch the next pagefetchPrevious()
- Manually fetch the previous pagehasNext()
- Whether there is a next pagehasPrevious()
- Whether there is a previous pageThe Inertia::scroll()
method provides server-side configuration for infinite scrolling. It automatically configures the proper merge behavior so that new data is appended or prepended to existing content instead of replacing it, and normalizes pagination metadata for the frontend component.
// Works with all Laravel pagination methods...
Inertia::scroll(User::paginate(20));
Inertia::scroll(User::simplePaginate(20));
Inertia::scroll(User::cursorPaginate(20));
// Works with API resources...
Inertia::scroll(UserResource::collection(User::paginate(20)));
If you don't use Laravel's paginator or use a different transformation layer, you may use the additional arguments that scroll()
accepts.
// Customize the data wrapper key (defaults to 'data')...
Inertia::scroll($customPaginatedData, wrapper: 'items');
// Provide custom metadata resolution...
Inertia::scroll($data, metadata: $metadataProvider);
The metadata parameter accepts an instance of ProvidesScrollMetadata
or a callback that returns such an instance. The callback receives the $data
parameter. This is useful when integrating with third-party pagination libraries like Fractal.
use League\Fractal\Resource\Collection;
class FractalScrollMetadata implements ProvidesScrollMetadata
{
public function __construct(protected Collection $resource) {}
public function getPageName(): string {}
public function getPreviousPage(): int|string|null {}
public function getNextPage(): int|string|null {}
public function getCurrentPage(): int|string|null {}
}
You may then use this custom metadata provider in your scroll function.
// Using an instance directly
Inertia::scroll($data, metadata: new FractalScrollMetadata($data));
// Using a callback
Inertia::scroll(
fn () => $this->transformData($data),
metadata: fn ($data) => new FractalScrollMetadata($data)
);
To avoid repeating this setup in multiple controllers, you may define a macro.
// In your AppServiceProvider's boot method
Inertia::macro('fractalScroll', function (Collection $data) {
return Inertia::scroll(
$data,
metadata: fn (Collection $data) => new FractalScrollMetadata($data)
);
});
// Then use it in your controllers
return Inertia::render('Users/Index', [
'users' => Inertia::fractalScroll($fractalCollection)
]);