Difficulty: Beginner–Intermediate | Read time: 10 min | Framework: CodeIgniter 4
The Problem with Default CI4 Pagination
CodeIgniter 4 has built-in pagination — but the default output looks nothing like Bootstrap 5. You get plain links with no styling, no active states, no prev/next arrows.
In every real project I've worked on, I end up customizing it. This tutorial shows you exactly how — from basic setup to a fully styled Bootstrap 5 paginator with AJAX support.
What we're building: A paginated list of records styled with Bootstrap 5, with custom prev/next buttons, active page highlighting, and optional AJAX loading.
Basic Setup First
Let's say you have a products table and want to paginate results.
1. The Model
<?php
// app/Models/ProductModel.php
namespace App\Models;
use CodeIgniter\Model;
class ProductModel extends Model
{
protected $table = 'products';
protected $primaryKey = 'id';
protected $allowedFields = ['name', 'price', 'category', 'created_at'];
public function getProducts(int $perPage = 10): array
{
return $this->paginate($perPage);
}
}
2. The Controller
<?php
// app/Controllers/Products.php
namespace App\Controllers;
use App\Models\ProductModel;
class Products extends BaseController
{
public function index(): string
{
$model = new ProductModel();
$perPage = 10;
$data['products'] = $model->getProducts($perPage);
$data['pager'] = $model->pager;
$data['perPage'] = $perPage;
return view('products/index', $data);
}
}
3. Basic View (before styling)
<!-- app/Views/products/index.php -->
<?php foreach ($products as $product): ?>
<div><?= esc($product['name']) ?> — ₹<?= $product['price'] ?></div>
<?php endforeach; ?>
<!-- Default pagination (unstyled) -->
<?= $pager->links() ?>
This works — but looks terrible. Let's fix that.
Custom Bootstrap 5 Pagination Template
CI4 lets you create a custom pagination template. This is the proper way to style it.
Step 1 — Create the template file
<!-- app/Views/pager/bootstrap_5.php -->
<?php $pager->setSurroundCount(2) ?>
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center mb-0">
<?php if ($pager->hasPreviousPage()): ?>
<li class="page-item">
<a class="page-link" href="<?= $pager->getPreviousPageURI() ?>" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
<?php else: ?>
<li class="page-item disabled">
<span class="page-link">«</span>
</li>
<?php endif ?>
<?php foreach ($pager->links() as $link): ?>
<li class="page-item <?= $link['active'] ? 'active' : '' ?>">
<?php if ($link['active']): ?>
<span class="page-link">
<?= $link['title'] ?>
<span class="visually-hidden">(current)</span>
</span>
<?php else: ?>
<a class="page-link" href="<?= $link['uri'] ?>">
<?= $link['title'] ?>
</a>
<?php endif ?>
</li>
<?php endforeach ?>
<?php if ($pager->hasNextPage()): ?>
<li class="page-item">
<a class="page-link" href="<?= $pager->getNextPageURI() ?>" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
<?php else: ?>
<li class="page-item disabled">
<span class="page-link">»</span>
</li>
<?php endif ?>
</ul>
</nav>
Step 2 — Register the template in config
// app/Config/Pager.php
public array $templates = [
'default_full' => 'CodeIgniter\Pager\Views\default_full',
'default_simple' => 'CodeIgniter\Pager\Views\default_simple',
'bootstrap_5' => 'App\Views\pager\bootstrap_5', // <-- add this line
];
Step 3 — Use it in your view
<!-- Now use your custom template -->
<?= $pager->links('default', 'bootstrap_5') ?>
That's it — fully Bootstrap 5 styled pagination! ✅
Extra Styling — Make It Look Even Better
Add this CSS to make the active page and hover states pop:
/* Custom pagination styling */
.pagination .page-link {
color: #6610f2;
border-color: #dee2e6;
padding: 8px 14px;
font-size: 14px;
transition: all 0.2s ease;
}
.pagination .page-item.active .page-link {
background-color: #6610f2;
border-color: #6610f2;
color: #fff;
}
.pagination .page-link:hover {
background-color: #f0ebff;
color: #6610f2;
border-color: #6610f2;
}
.pagination .page-item.disabled .page-link {
color: #adb5bd;
pointer-events: none;
}
Show Record Count (Very Useful in Real Projects)
Users want to know "Showing 1–10 of 154 records". Here's how:
// In your controller — add these to $data
$data['total'] = $model->countAllResults();
$data['currentPage'] = $model->pager->getCurrentPage();
$data['perPage'] = $perPage;
<!-- In your view -->
<?php
$start = (($currentPage - 1) * $perPage) + 1;
$end = min($currentPage * $perPage, $total);
?>
<div class="d-flex justify-content-between align-items-center mb-3">
<small class="text-muted">
Showing <?= $start ?>–<?= $end ?> of <?= $total ?> records
</small>
<?= $pager->links('default', 'bootstrap_5') ?>
</div>
Output: Showing 1–10 of 154 records with pagination on the right. Clean and professional!
AJAX Pagination (No Page Reload)
For a smoother experience, load pages via AJAX:
<!-- In your view -->
<div id="products-wrapper">
<?= view('products/_table', ['products' => $products]) ?>
</div>
<div id="pagination-wrapper">
<?= $pager->links('default', 'bootstrap_5') ?>
</div>
<script>
$(document).on('click', '#pagination-wrapper .page-link', function (e) {
e.preventDefault();
const url = $(this).attr('href');
if (!url || url === '#') return;
$.get(url, function (response) {
const html = $(response);
$('#products-wrapper').html(html.find('#products-wrapper').html());
$('#pagination-wrapper').html(html.find('#pagination-wrapper').html());
// Scroll to top of table smoothly
$('html, body').animate({ scrollTop: $('#products-wrapper').offset().top - 80 }, 300);
});
});
</script>
Common Pitfalls
⚠️ 1. Pagination not working with WHERE filters
If you apply filters before paginate(), always use $model->where() before calling it:
// WRONG — filter applied after, pagination counts wrong
$data['products'] = $model->paginate($perPage);
$model->where('category', 'electronics');
// CORRECT — filter first, then paginate
$data['products'] = $model->where('category', $category)
->paginate($perPage);
$data['pager'] = $model->pager;
⚠️ 2. Template not found error
Make sure the path in Pager.php config matches exactly:
'bootstrap_5' => 'App\Views\pager\bootstrap_5'
// File must be at: app/Views/pager/bootstrap_5.php
⚠️ 3. Page resets to 1 on form submit
When using search + pagination together, pass the current page in your form:
<input type="hidden" name="page" value="<?= $currentPage ?>">
And in controller:
$page = $this->request->getGet('page') ?? 1;
$data['products'] = $model->where('name LIKE', "%{$search}%")
->paginate($perPage, 'default', $page);
Full Working Example — Products Page
Here's everything together in one clean view:
<!-- app/Views/products/index.php -->
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="mb-0">Products</h4>
<small class="text-muted">
Showing <?= $start ?>–<?= $end ?> of <?= $total ?> records
</small>
</div>
<div class="table-responsive" id="products-wrapper">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>#</th>
<th>Product Name</th>
<th>Category</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<?php foreach ($products as $p): ?>
<tr>
<td><?= $p['id'] ?></td>
<td><?= esc($p['name']) ?></td>
<td><span class="badge bg-secondary"><?= esc($p['category']) ?></span></td>
<td>₹<?= number_format($p['price'], 2) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div id="pagination-wrapper" class="mt-3">
<?= $pager->links('default', 'bootstrap_5') ?>
</div>
</div>
What to Read Next
- CodeIgniter 4 DataTables Server-Side Processing — for large datasets with search/sort
- CodeIgniter 4 + jQuery AJAX CRUD — full AJAX without page reload
- Dynamic Report with Date Range Filters in CodeIgniter — pagination with complex filters
Conclusion
CI4's pagination is powerful once you know how to configure it. A custom Bootstrap 5 template + the record count display makes your app look polished and professional — exactly what clients expect.
Follow me for more CodeIgniter production tutorials — 3 new articles every week. 🙌
Senior PHP Developer · 12+ years building production systems on CodeIgniter, Laravel & WordPress
Top comments (0)