Infinite Scroll
This recipe demonstrates implementing infinite scroll pagination using Datastar's intersection observer to load more content automatically as users scroll to the bottom of the page.
What We're Building
An article feed with:
- Automatic loading on scroll
- Load more button as fallback
- Loading indicator
- End-of-results detection
- Performance-optimized scroll detection
Complete Implementation
Blade Template
Create resources/views/articles/feed.blade.php:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Articles Feed</title>
@hyper
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-50">
<div class="max-w-3xl mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-8">Articles Feed</h1>
<div @signals([
'_articles' => $articles->items(),
'newArticles' => [],
'page' => $articles->currentPage(),
'hasMore' => $articles->hasMorePages(),
'_loading' => false
])
data-effect="if ($newArticles && $newArticles.length > 0) { $_articles = [...$_articles, ...$newArticles]; $newArticles = []; }"
data-on:scroll__window__throttle.300ms="
if ($hasMore && !$_loading) {
const scrollPosition = window.innerHeight + window.scrollY;
const pageHeight = document.body.offsetHeight;
const triggerPoint = pageHeight - 600;
if (scrollPosition >= triggerPoint) {
$_loading = true;
$page = $page + 1;
@get('/articles/feed');
}
}
">
<div class="space-y-6 mb-8">
<template data-for__key.id="article in $_articles">
<article class="bg-white rounded-lg shadow p-6">
<h2 class="text-xl font-bold mb-2" data-text="article.title"></h2>
<p class="text-sm text-gray-500 mb-3" data-text="article.author"></p>
<p class="text-gray-700 mb-4" data-text="article.excerpt"></p>
<a data-attr:href="'/articles/' + article.id" class="text-blue-600 hover:text-blue-800">
Read more →
</a>
</article>
</template>
</div>
<div data-show="$_loading" class="text-center py-8">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<p class="text-gray-500 mt-2">Loading more articles...</p>
</div>
<div data-show="$hasMore && !$_loading" class="text-center py-8">
<button
data-on:click="$_loading = true; $page = $page + 1; @get('/articles/feed')"
class="px-6 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors">
Load More Articles
</button>
</div>
<div data-show="!$hasMore && $_articles.length > 0" class="text-center py-8">
<p class="text-gray-500 text-lg font-semibold">You've reached the end! 🎉</p>
</div>
</div>
</div>
</body>
</html>Controller
Create app/Http/Controllers/ArticleController.php:
<?php
namespace App\Http\Controllers;
use App\Models\Article;
use Illuminate\Http\Request;
class ArticleController extends Controller
{
public function feed(Request $request)
{
if ($request->isHyper()) {
$page = (int) signals('page', 1);
$articles = Article::latest()->paginate(10, ['*'], 'page', $page);
$items = $articles->items();
return hyper()->signals([
'newArticles' => is_array($items) ? $items : $items->toArray(),
'page' => $articles->currentPage(),
'hasMore' => $articles->hasMorePages(),
'_loading' => false
]);
}
$articles = Article::latest()->paginate(10);
return view('articles.feed', compact('articles'));
}
}Route
Inside your web.php
use App\Http\Controllers\ArticleController;
Route::get('/articles', [ArticleController::class, 'feed']);How It Works
Datastar's data-on:scroll__window listens to scroll events and checks your position:
<div
data-on:scroll__window__throttle.300ms="
if ($hasMore && !$_loading) {
const scrollPosition = window.innerHeight + window.scrollY;
const pageHeight = document.body.offsetHeight;
const triggerPoint = pageHeight - 600;
if (scrollPosition >= triggerPoint) {
$_loading = true;
$page = $page + 1;
@get('/articles/feed');
}
}
">
</div>Breakdown: • data-on:scroll__window - Listens to window scroll events
• __throttle.300ms - Only checks every 300 milliseconds (performance optimization)
• scrollPosition = window.innerHeight + window.scrollY - Calculates where the bottom of your viewport is
• triggerPoint = pageHeight - 600 - Sets trigger line 600px before page end
• if (scrollPosition >= triggerPoint) - When you scroll within 600px of bottom, loads more

