惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
雷峰网
雷峰网
Hugging Face - Blog
Hugging Face - Blog
IT之家
IT之家
H
Help Net Security
腾讯CDC
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
The GitHub Blog
The GitHub Blog
V
V2EX
M
MIT News - Artificial intelligence
Vercel News
Vercel News
WordPress大学
WordPress大学
博客园 - 三生石上(FineUI控件)
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
阮一峰的网络日志
阮一峰的网络日志
B
Blog RSS Feed
D
Docker
V
Visual Studio Blog
博客园 - 叶小钗
美团技术团队
S
SegmentFault 最新的问题
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
CodeIgniter 4 Pagination — Custom Styling with Bootstrap 5
sunakshi Thakur · 2026-06-26 · via DEV Community

sunakshi Thakur

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">&laquo;</span>
        </a>
      </li>
    <?php else: ?>
      <li class="page-item disabled">
        <span class="page-link">&laquo;</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">&raquo;</span>
        </a>
      </li>
    <?php else: ?>
      <li class="page-item disabled">
        <span class="page-link">&raquo;</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