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.

About trencq

Hi there 👋, I’m Delowar Hossen (QuantumCloud)
-------------------------------------------
đŸŒ± I’m currently creating a sample Laravel and WordPress plugin
👯 I’m looking to collaborate on open-source PHP & JavaScript projects
💬 Ask me about Laravel, MySQL and WordPress plugin
⚡ Fun fact: I love turning ☕ into code!

Leave a Comment

Your email address will not be published. Required fields are marked *

Categories

Tags

Scroll to Top