How Does Infinite Scroll Work in CodeIgniter 4 Using AJAX and jQuery? (Step-by-Step Overview)
Implementing infinite scroll in CodeIgniter 4 follows a clean client-server pattern. Here's how it works end-to-end:
- Initial Page Load: The main controller method (e.g.,
UserController::index()) fetches the first page of records using the model'spaginate()or manuallimit()/offset(). It passes data to the view, which renders cards/items in a container div (e.g.,#users-container). - Scroll Detection: jQuery's
$(window).scroll()event monitors position. When the user nears the bottom (e.g.,scrollTop + window height > document height - 300), it triggers a function if not already loading. - AJAX Request: An AJAX GET/POST call sends the next
pagenumber to a dedicated endpoint (e.g.,/users/load-more?page=2). Flags likeisLoadingprevent duplicate requests during slow networks. - Server-Side Handling: The controller receives the page, calculates offset (
(page-1) * perPage), queries the database for the next batch, and returns a partial view (e.g.,load_users.php) containing only the new HTML items — no full layout. - Dynamic Append & State Update: On success, jQuery appends the returned HTML to the container, increments the page counter, hides the loader, and checks if more data exists (empty response = stop loading, show "No more" message).
- Enhancements: Add a spinner for feedback, handle errors gracefully, debounce the scroll event for performance, and optionally use Intersection Observer for a jQuery-free modern alternative.
This method ensures smooth, efficient data loading without page refreshes. With proper CSRF handling (if enabled) and accessibility considerations (e.g., aria-live regions for new content), it's robust for production. The entire flow keeps your CodeIgniter 4 app fast, scalable, and user-friendly for large datasets.

Table Of Content
1 Prerequisites
- PHP ≥ 8.1
- Composer
- MySQL database
- Basic knowledge of CodeIgniter 4 (Models, Controllers, Views, Migrations)
2 Introduction
3 Create / Install a Codeigniter 4 Project
3.1 Install Codeigniter 4 Project
composer create-project codeigniter4/appstarter ci-4-ajax-load-more-app
Then, navigate to your project directory:
cd ci-4-ajax-load-more-app
3.2 Configure Environment (.env)
sudo cp env .env
Switch to development mode by updating the .env file:
# CI_ENVIRONMENT = production
CI_ENVIRONMENT = development
database.default.hostname = localhost
database.default.database = ci4_infinite_scroll_db # Create this DB in MySQL
database.default.username = root
database.default.password =
database.default.DBDriver = MySQLi
Now application is in development mode.
4 Create Migration and Model
Create a migration file for the users table:
php spark make:migration CreateUsersTable
Define the table structure in the migration file.
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateUsersTable extends Migration
{
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'BIGINT',
'unsigned' => true,
'auto_increment' => true,
],
'name' => [
'type' => 'VARCHAR',
'constraint' => '255',
],
'email' => [
'type' => 'VARCHAR',
'constraint' => '255',
],
'created_at' => [
'type' => 'TIMESTAMP',
'null' => true,
],
'updated_at' => [
'type' => 'TIMESTAMP',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->createTable('users');
}
public function down()
{
$this->forge->dropTable('users');
}
}
Run the migration:
php spark migrate
php spark make:model UserModel
Edit UserModel.php to define the users table structure. Then, create the migration for the table:
<?php
namespace App\Models;
use CodeIgniter\Model;
class UserModel extends Model
{
protected $table = 'users';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $allowedFields = ['name', 'email', 'created_at', 'updated_at'];
// Dates
protected $useTimestamps = false;
protected $dateFormat = 'datetime';
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $deletedField = 'deleted_at';
// Validation
protected $validationRules = [];
protected $validationMessages = [];
protected $skipValidation = false;
protected $cleanValidationRules = true;
// Callbacks
protected $allowCallbacks = true;
protected $beforeInsert = [];
protected $afterInsert = [];
protected $beforeUpdate = [];
protected $afterUpdate = [];
protected $beforeFind = [];
protected $afterFind = [];
protected $beforeDelete = [];
protected $afterDelete = [];
}
5 Create Database Seeder
Run this command:
php spark make:seeder UserSeeder
Edit UserSeeder.php to generate 100 fake users using the Faker library, then run the seeder:
<?php
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
use CodeIgniter\I18n\Time;
use Faker\Factory;
class UserSeeder extends Seeder
{
public function run()
{
$faker = Factory::create();
$model = model('App\Models\UserModel');
for ($i = 0; $i < 100; $i++) {
$model->insert([
'name' => $faker->name,
'email' => $faker->unique()->safeEmail,
'created_at' => $faker->dateTime->format('Y-m-d H:i:s'),
'updated_at' => Time::now()->toDateTimeString(),
]);
}
}
}
Run this below command to insert the data.
php spark db:seed UserSeeder
6 Create New Controller - UserController
php spark make:controller UserController
In UserController.php, define methods to load data on page scroll. The loadMore method will handle Ajax requests, loading more data based on the page offset.
<?php
namespace App\Controllers;
use App\Models\UserModel;
use CodeIgniter\API\ResponseTrait;
class UserController extends BaseController
{
use ResponseTrait;
protected $userModel;
public function __construct()
{
$this->userModel = new UserModel();
}
public function index()
{
$perPage = 8; // Initial load
$data['users'] = $this->userModel->orderBy('id', 'DESC')->findAll($perPage, 0);
return view('users/index', $data);
}
public function loadMore()
{
$page = $this->request->getGet('page') ?? 2;
$perPage = 8;
$offset = ($page - 1) * $perPage;
$users = $this->userModel->orderBy('id', 'DESC')->findAll($perPage, $offset);
if (empty($users)) {
return $this->respond(''); // No more data
}
return view('users/load_more', ['users' => $users]);
}
}
?>
7 Create a View
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CodeIgniter 4 Infinite Scroll: Load More Data on Scroll with AJAX & jQuery</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
</head>
<body class="bg-light">
<div class="container my-5">
<h1 class="mb-4">CodeIgniter 4: Load More Data on Page Scroll with AJAX and jQuery (Infinite Scroll)</h1>
<div id="users-container">
<?php foreach ($users as $user): ?>
<div class="card mb-3 shadow-sm">
<div class="card-body">
<h5 class="card-title"><?= esc($user['name']) ?></h5>
<p class="card-text text-muted"><?= esc($user['email']) ?></p>
</div>
</div>
<?php endforeach; ?>
</div>
<div id="loading" class="text-center my-4" style="display: none;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p>Loading more users...</p>
</div>
<div id="no-more" class="text-center my-4 text-muted" style="display: none;">
No more users to load.
</div>
</div>
<script>
let page = 2;
let isLoading = false;
let hasMore = true;
$(window).on('scroll', function() {
if ($(window).scrollTop() + $(window).height() > $(document).height() - 300 && !isLoading && hasMore) {
loadMoreData();
}
});
function loadMoreData() {
isLoading = true;
$('#loading').show();
$.ajax({
url: '<?= base_url('users/load-more') ?>',
type: 'GET',
data: { page: page },
success: function(data) {
if (data.trim() === '') {
hasMore = false;
$('#no-more').show();
} else {
$('#users-container').append(data);
page++;
}
$('#loading').hide();
isLoading = false;
},
error: function(xhr, status, error) {
console.error('AJAX Error:', error);
$('#loading').hide();
isLoading = false;
}
});
}
</script>
</body>
</html>
Create a view file load_more.php in the app/Views directory to display the load more data:
<?php foreach ($users as $user): ?>
<div class="card mb-3 shadow-sm">
<div class="card-body">
<h5 class="card-title"><?= esc($user['name']) ?></h5>
<p class="card-text text-muted"><?= esc($user['email']) ?></p>
</div>
</div>
<?php endforeach; ?>
8 Define a Route
use CodeIgniter\Router\RouteCollection;
/**
* @var RouteCollection $routes
*/
$routes->get('/', 'Home::index');
$routes->get('users', 'UserController::index');
$routes->get('users/load-more', 'UserController::loadMore');
9 Folder Structure
10 Run Web Server to Test the App
php spark serve
Visit http://localhost:8080/index.php to see the app in action.
11 Conclusion
Implementing infinite scroll (load more data on page scroll using AJAX and jQuery) in CodeIgniter 4 is an excellent way to modernize your web applications and deliver a smoother, more engaging user experience. By replacing traditional pagination with seamless, automatic content loading, you eliminate page reloads, reduce user friction, and encourage visitors to explore more of your content effortlessly — whether it's a product catalog, blog archive, news feed, or user directory.
In this complete tutorial, we covered every essential step: setting up a fresh CodeIgniter 4 project, creating the database table and seeding sample data, building an efficient UserModel, handling pagination logic in the controller (both initial load and AJAX endpoint), defining clean routes, crafting responsive views with Bootstrap cards, and writing robust jQuery code to detect scroll position, fetch additional records asynchronously, append them dynamically, and manage loading states with a spinner and “no more data” message.
The result is a lightweight, performant solution that works beautifully on both desktop and mobile devices without relying on heavy frontend frameworks. You now have a production-ready infinite scroll feature that can be easily adapted to any model or dataset in your CodeIgniter 4 application. For even better performance, consider adding scroll event debouncing, switching to the native Intersection Observer API (to remove jQuery dependency), implementing lazy loading for images inside the cards, or adding browser history support with pushState for better shareability and SEO.
As web users increasingly expect fluid, app-like experiences in 2026, mastering techniques like AJAX-powered infinite scroll gives your projects a clear competitive edge. Test the implementation thoroughly, monitor page speed and bounce rates, and watch user engagement improve. Happy coding — your CodeIgniter 4 apps just got a lot more addictive!
Quick recap of benefits you now enjoy:
- Improved user retention and time-on-page
- Faster initial page loads with incremental data fetching
- Mobile-friendly scrolling behavior
- Easy integration into existing CI4 projects
- Foundation for advanced features like pull-to-refresh or virtual scrolling
Thanks for following along — feel free to share your results, ask questions in the comments, or extend this pattern to real-time feeds with WebSockets. Keep building awesome things!
Written by Revathi M
PHP Developer & Technical Writer · 10+ years building web applications with CodeIgniter and Laravel
Revathi specializes in PHP backend development, authentication systems, and REST API design. She writes practical, production-tested tutorials at Get Sample Code to help developers build secure applications faster.
Frequently Asked Questions
You need PHP 8.1+, Composer, MySQL, and a fresh CodeIgniter 4 installation. Basic knowledge of migrations, models, controllers, views, and jQuery is required.
Create a migration for the 'users' table with fields: id (BIGINT unsigned auto_increment), name (VARCHAR 255), email (LONGTEXT), created_at and updated_at (TIMESTAMP nullable). Run php spark migrate and seed 100 fake records using UserSeeder with Faker.
In UserController, onScrollLoadMore() gets the page parameter, calculates offset ($start = 4 * (page - 1)), fetches data with limit 4 and offset, and returns the partial 'load_users' view.
jQuery detects when scroll is near bottom ($(window).scrollTop() + $(window).height() >= $(document).height() - 555). It increments page, prevents multiple loads with a flag, and AJAX GETs 'onScrollLoadMore?page=' + page to append HTML.
In index.php, show initial users in Bootstrap cards inside #loadMoreBlock. In load_users.php (partial), loop through $users and output each name in a card.
Common issues: jQuery CDN not loaded, incorrect base_url in JS, route mismatch, or database empty. Check console for errors, ensure isLoading flag works, and verify offset calculation.
If AJAX returns empty data, set triggerScrollLoader to false to stop further scroll events and hide the loader.
The JavaScript shows/hides a #loader element, but the tutorial HTML does not include it—add a
Yes, it uses Bootstrap 5 for card styling (card mb-3). Include Bootstrap CDN in the main view.
Add a visible loading spinner, error handling in AJAX fail(), caching, or use Intersection Observer API instead of jQuery scroll for better performance.
