Laravel Table Pagination with Bootstrap 5

Laravel Table Pagination with Bootstrap 5

Pagination Feature

You’ve implemented a fully dynamic table with pagination, sorting, searching, and adjustable entries per page using:

  • Blade View: Clean Bootstrap 5 UI

  • AJAX: Fetches paginated data without reload

  • JavaScript: Handles DOM rendering and pagination interaction

  • Controller: WizardFormController handles the logic and returns paginated JSON

  • Routing: Uses route groups and authentication middleware

  • Optional CSS: For better UX with sticky headers and styled buttons

Include CSS in Blade View

In resources/views/wizardform/pagination.blade.php, add in the <head>:

@extends('layouts.master') @section('content') <div class="row"> <div class="col-lg-12 col-md-8 col-sm-12 mx-auto"> <div class="d-flex justify-content-between align-items-center mb-3"> <div class="fw-bold py-2">Pagination</div> <div class="p-2"> Pagination - <a href="{{ route('home') }}" class="text-decoration-none fw-semibold">Dashboard</a> </div> </div> <div class="card p-3 shadow-sm"> <div class="table-responsive"> <!-- Top: Search + Show Entries --> <div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2"> <!-- Left: Show entries --> <div class="d-flex align-items-center gap-2"> <label for="entriesPerPage" class="form-label mb-0 small">Show</label> <select id="entriesPerPage" class="form-select form-select-sm"> <option value="5">5</option> <option value="10" selected>10</option> <option value="25">25</option> <option value="50">50</option> </select> <span class="small">entries</span> </div> <!-- Right: Search input --> <div class="d-flex align-items-center gap-2"> <input type="text" id="searchInput" class="form-control form-control-sm" placeholder="Search..."> </div> </div> <!-- Table --> <div id="tableScrollWrapper"> <table class="table table-striped nowrap"> <thead> <tr id="tableHead"> <!-- Generated dynamically --> </tr> </thead> <tbody id="tableBody"> <!-- Generated dynamically --> </tbody> </table> </div> <!-- Pagination + Go to + Info --> <nav class="container-fluid"> <div class="row align-items-center justify-content-between flex-wrap gap-2"> <!-- Left: Range Info --> <div class="col-auto"> <div class="small text-muted" id="rangeInfo"></div> </div> <!-- Center: Pagination + Go to input --> <div class="col"> <div class="d-flex justify-content-center align-items-center flex-wrap gap-3"> <ul class="pagination mb-0" id="pagination"> <!-- Filled by JS --> </ul> <div class="d-flex align-items-center gap-2"> <input type="number" id="jumpPage" class="form-control form-control-sm" min="1" style="width: 60px !important;"> <span class="text-muted small">of <span id="totalItems"></span> records</span> </div> </div> </div> </div> </nav> </div> </div> </div> </div> @endsection @section('script') <script> let data = [], rowsPerPage = 10, currentPage = 1; let sortColumn = null, sortDirection = 'asc'; const excludedColumns = []; function fetchData() { $.getJSON('form/get-data/listing', { page: currentPage, per_page: rowsPerPage, search: $('#searchInput').val(), sort_by: sortColumn || 'id', sort_dir: sortDirection }, function (response) { data = response.rows; renderTableHeader(response.columns); renderTable(data); renderPagination(response.total); }); } function renderTableHeader(columns) { const headerHtml = ['<th>Action</th>', ...Object.entries(columns) .filter(([col]) => !excludedColumns.includes(col)) .map(([col, label]) => `<th class="sortable" data-sort="${col}">${label}</th>`)] .join(''); $('#tableHead').html(headerHtml); } function renderTable(rows) { const html = rows.map(user => { const cells = Object.entries(user) .filter(([col]) => !excludedColumns.includes(col)) .map(([col, val]) => { if (col === 'status') { return `<td><span class="badge ${val === 'Active' ? 'bg-success-subtle text-success' : 'bg-danger-subtle text-danger'}">${val}</span></td>`; } return `<td>${val}</td>`; }).join(''); return `<tr> <td> <button class="btn btn-sm btn-info me-1"><i class="bi bi-eye"></i></button> <button class="btn btn-sm btn-primary me-1"><i class="bi bi-pencil-square"></i></button> <button class="btn btn-sm btn-danger"><i class="bi bi-trash"></i></button> </td> ${cells} </tr>`; }).join(''); $('#tableBody').html(html); } function renderPagination(totalItems) { const totalPages = Math.ceil(totalItems / rowsPerPage); let start = Math.max(1, currentPage - 2); let end = Math.min(totalPages, start + 4); if (end - start < 4) start = Math.max(1, end - 4); const addBtn = (label, page, disabled = false, active = false) => ` <li class="page-item ${disabled ? 'disabled' : ''} ${active ? 'active' : ''}"> <a class="page-link" href="#" data-page="${page}">${label}</a> </li>`; let pagination = addBtn('«', 1, currentPage === 1) + addBtn('‹', currentPage - 1, currentPage === 1); for (let i = start; i <= end; i++) { pagination += addBtn(i, i, false, currentPage === i); } pagination += addBtn('›', currentPage + 1, currentPage === totalPages) + addBtn('»', totalPages, currentPage === totalPages); $('#pagination').html(pagination); $('#rangeInfo').text(`${(currentPage - 1) * rowsPerPage + 1}–${Math.min(currentPage * rowsPerPage, totalItems)} of ${totalItems} records`); $('#totalItems').text(totalItems); } function updateSortIcons() { $('.sortable').removeClass('sorted-asc sorted-desc').each(function () { if ($(this).data('sort') === sortColumn) { $(this).addClass(sortDirection === 'asc' ? 'sorted-asc' : 'sorted-desc'); } }); } // Event Listeners $(document).ready(() => { fetchData(); $('#entriesPerPage').on('change', function () { rowsPerPage = +this.value; currentPage = 1; fetchData(); }); $('#jumpPage').on('keypress', function (e) { if (e.key === 'Enter') { let page = +$(this).val(); let totalPages = Math.ceil(data.length / rowsPerPage); if (page >= 1 && page <= totalPages) { currentPage = page; fetchData(); $(this).val(''); } } }); $('#searchInput').on('input', function () { currentPage = 1; fetchData(); }); }); $(document).on('click', '#pagination .page-link', function (e) { e.preventDefault(); const page = +$(this).data('page'); if (!isNaN(page)) { currentPage = page; fetchData(); } }); $(document).on('click', '.sortable', function () { const col = $(this).data('sort'); if (sortColumn === col) { sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'; } else { sortColumn = col; sortDirection = 'asc'; } fetchData(); updateSortIcons(); }); </script>
@endsection

Add Route (routes/web.php)

Add routes/web.php

// ------------------------- Wizard Form ----------------------------// Route::controller(WizardFormController::class)->group(function () { Route::middleware('auth')->group(function () { Route::get('form/pagination/page', 'index')->name('form/pagination/page'); Route::get('form/pagination/form/get-data/listing', 'getData')->name('form/pagination/form/get-data/listing'); }); });

Create WizardFormController Controller

php artisan make:controller WizardFormController

WizardFormController Logic

Edit app/Http/Controllers/WizardFormController.php

<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use DB; class WizardFormController extends Controller { public function index() { return view('wizardform.pagination'); } public function getData(Request $request) { $perPage = $request->input('per_page', 10); $page = $request->input('page', 1); $search = $request->input('search', ''); $sortBy = $request->input('sort_by', 'id'); $sortDir = $request->input('sort_dir', 'asc'); // Define database columns and their display labels $columns = ['id', 'user_id', 'name', 'email', 'position', 'department', 'status','created_at']; $columnLabels = [ 'id' => 'ID', 'user_id' => 'User ID', 'name' => 'Name', 'email' => 'Email', 'position' => 'Position', 'department' => 'Department', 'status' => 'Status', 'created_at' => 'Created At', ]; // Start the base query $query = DB::table('users')->select($columns); // Apply search filtering if ($search) { $query->where(function ($q) use ($columns, $search) { foreach ($columns as $col) { $q->orWhere($col, 'like', "%{$search}%"); } }); } // Get total count **before pagination** $total = $query->count(); // Apply sorting and pagination $users = $query->orderBy($sortBy, $sortDir) ->offset(($page - 1) * $perPage) ->limit($perPage) ->get(); return response()->json([ 'columns' => $columnLabels, 'rows' => $users, 'total' => $total, ]); } }

Optional Custom CSS (inline or in your public/css)

/* table custome */ .th-active-fixed { position: sticky; left: 0; background: white; z-index: 2; } .margin-top-center { margin-top: 4px !important; } .custom-margin-top { margin-top: -5px !important; } .height-custom { height: 42px !important; } .page-link { border-radius: 8px !important; margin-left: 2px !important; margin-right: 2px !important; } .sortable { cursor: pointer; position: relative; } .sortable::after { content: ' ⇅'; font-size: 0.8em; color: #888; } .sortable.sorted-asc::after { content: ' ↑'; } .sortable.sorted-desc::after { content: ' ↓'; } .table { --bs-table-border-color: #63636363 !important; } #tableScrollWrapper { overflow-x: auto; } #tableScrollWrapper table { min-width: 1000px; } #tableScrollWrapper th, #tableScrollWrapper td { white-space: nowrap; } /* Make the first column (Action) sticky */ #tableHead th:first-child, #tableBody td:first-child { position: sticky; left: 0; background: #fff; /* Match your design */ z-index: 1; box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1); } .pagination { --bs-pagination-color: #4361eecc !important; --bs-pagination-hover-color: #4361eecc !important; --bs-pagination-hover-border-color: #4361eecc !important; --bs-pagination-focus-color: #4361eecc !important; --bs-pagination-focus-bg: #4361eecc !important; --bs-pagination-focus-box-shadow: #4361eecc !important; --bs-pagination-active-color: #FFFFFF !important; --bs-pagination-active-bg: #4361eecc !important; --bs-pagination-active-border-color: #4361eecc !important; } .pagination-dark .page-link { background-color: #333; color: #fff; border-color: #444; } .pagination-dark .page-link:hover { background-color: #444; color: #fff; } .pagination-dark .page-item.disabled .page-link { background-color: #333; color: #aaa; border-color: #444; }

Blade View (resources/views/wizardform/pagination.blade.php)

  • Extends layouts.master.

  • Displays:

    • Pagination title and back-to-dashboard link.

    • Search input and entries-per-page dropdown.

    • Dynamic table (#tableHead, #tableBody).

    • Pagination UI with jump-to-page and range info.

  • Uses Bootstrap 5 classes for styling.

JavaScript Logic

  • Fetches data via AJAX (/form/get-data/listing) using $.getJSON.

  • Features:

    • Dynamic sorting on column headers.

    • Search filtering.

    • Entries per page selection.

    • Jump to specific page.

    • Sticky action column.

  • Pagination controls update based on the total records and current page.

Controller (WizardFormController.php)

  • index(): Returns the pagination view.

  • getData(Request $request):

    • Accepts pagination, search, and sort parameters.

    • Queries users table dynamically.

    • Returns JSON: columns, rows, total.

Optional Custom CSS

Enhances UX:

  • Sticky action column (th:first-child / td:first-child).

  • Sortable columns with up/down arrows.

  • Compact pagination buttons.

  • Responsive table with horizontal scroll.

Highlights

  • Clean, dynamic, and responsive UI.

  • Full client-side control via jQuery.

  • Server-side pagination + filtering + sorting.

  • Easily customizable for any dataset.

Souy Soeng

Souy Soeng

Hi there 👋, I’m Soeng Souy (StarCode Kh)
-------------------------------------------
🌱 I’m currently creating a sample Laravel and React Vue Livewire
👯 I’m looking to collaborate on open-source PHP & JavaScript projects
💬 Ask me about Laravel, MySQL, or Flutter
⚡ Fun fact: I love turning ☕️ into code!

Post a Comment

CAN FEEDBACK
close