DEV Community

sunakshi Thakur
sunakshi Thakur

Posted on

CodeIgniter 4 Pagination — Custom Styling with Bootstrap 5

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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() ?>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
];
Enter fullscreen mode Exit fullscreen mode

Step 3 — Use it in your view

<!-- Now use your custom template -->
<?= $pager->links('default', 'bootstrap_5') ?>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
<!-- 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

⚠️ 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
Enter fullscreen mode Exit fullscreen mode

⚠️ 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 ?>">
Enter fullscreen mode Exit fullscreen mode

And in controller:

$page = $this->request->getGet('page') ?? 1;
$data['products'] = $model->where('name LIKE', "%{$search}%")
                           ->paginate($perPage, 'default', $page);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)