Updated June 2026. Tested on Laravel 13 and PHP 8.4.

Eloquent paginates for free with ->paginate(). But sometimes your data does not come from Eloquent. It might be the result of a complex raw query or a stored procedure, sitting in a plain array or collection. Laravel still lets you paginate it, you just build the paginator yourself with LengthAwarePaginator.

Build the paginator

The idea is simple: figure out the current page, slice your data to just that page, and wrap it in a LengthAwarePaginator that knows the total count.

use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Illuminate\Http\Request;

public function index(Request $request)
{
    // your data from anywhere: a raw query, a stored procedure, an API
    $results = collect(DB::select('CALL my_report(?)', [$request->input('month')]));

    $perPage     = 15;
    $currentPage = LengthAwarePaginator::resolveCurrentPage();

    $pageItems = $results->slice(($currentPage - 1) * $perPage, $perPage)->values();

    $paginated = new LengthAwarePaginator(
        $pageItems,
        $results->count(),   // total across all pages
        $perPage,
        $currentPage,
        ['path' => $request->url(), 'query' => $request->query()]
    );

    return view('reports.index', ['rows' => $paginated]);
}

Two details matter. Passing $currentPage as the fourth argument means the paginator knows which page it is on. Passing path and query in the options means the page links keep your other query parameters (like filters) instead of dropping them.

Show it in the view

It behaves exactly like an Eloquent paginator, so the view is the same.

@foreach ($rows as $row)
    <p>{{ $row->name }}</p>
@endforeach

{{ $rows->links() }}

->links() renders the page navigation, styled with Tailwind by default.

Making it work over AJAX

To page without a full reload, intercept the clicks on the page links, fetch the next page, and swap in the new HTML. With plain JavaScript, no jQuery needed:

document.addEventListener('click', async (e) => {
    const link = e.target.closest('.pagination a');
    if (! link) return;

    e.preventDefault();
    const response = await fetch(link.href, {
        headers: { 'X-Requested-With': 'XMLHttpRequest' },
    });
    document.querySelector('#results').innerHTML = await response.text();
});

On the controller side, return just the results partial when the request is AJAX.

if ($request->ajax()) {
    return view('reports._rows', ['rows' => $paginated])->render();
}

That is all there is to it. LengthAwarePaginator lets you paginate anything, not just Eloquent, and once it is built it plugs into the same view helpers and AJAX flow you already use. Questions welcome in the comments.