HTMX Infinite Scroll
Infinite scroll loads more content automatically as the user scrolls toward the bottom of the page. Social media feeds, news sites, and product listings all use this pattern. With HTMX, you build infinite scroll without a single line of custom JavaScript — using only the revealed trigger and server-side pagination logic.
How Infinite Scroll Works
Page loads → shows first 10 posts
User scrolls down...
|
v
A hidden "Load More" sentinel div enters the viewport
|
v
HTMX sees the sentinel and fires GET /posts?page=2
|
v
Server returns posts 11–20 PLUS a new sentinel for page 3
|
v
HTMX appends the new posts and the new sentinel to the list
|
v
User keeps scrolling → sentinel for page 3 enters viewport
|
v
Cycle repeats until no more pages exist
The Sentinel Pattern
The sentinel is a small element placed after the last item in the list. It has no visible content. Its only job is to watch for when it enters the viewport and then trigger the next page load.
<ul id="post-list">
<li>Post 1</li>
<li>Post 2</li>
<li>Post 3</li>
...
<li>Post 10</li>
<!-- Sentinel — loads page 2 when scrolled into view -->
<li
hx-get="/posts?page=2"
hx-trigger="revealed"
hx-target="#post-list"
hx-swap="beforeend">
<span class="htmx-indicator">Loading more...</span>
</li>
</ul>
Diagram: ┌──────────────────────────────┐ ← viewport top │ Post 1 │ │ Post 2 │ │ Post 3 │ │ ... │ │ Post 10 │ │ [sentinel — hidden] │ ← not yet in viewport └──────────────────────────────┘ ← viewport bottom User scrolls... ┌──────────────────────────────┐ │ Post 7 │ │ Post 8 │ │ Post 9 │ │ Post 10 │ │ [sentinel — REVEALED!] │ ← enters viewport → fires request └──────────────────────────────┘
Server Response for Page 2
The server returns the next batch of posts plus a new sentinel pointing to page 3. HTMX appends both to the list.
<!-- Server response for /posts?page=2 --> <li>Post 11</li> <li>Post 12</li> <li>Post 13</li> ... <li>Post 20</li> <li hx-get="/posts?page=3" hx-trigger="revealed" hx-target="#post-list" hx-swap="beforeend"> <span class="htmx-indicator">Loading more...</span> </li>
When there are no more pages, the server returns only the last batch of posts — no sentinel. The cycle ends naturally.
Server-Side Implementation
Flask (Python)
@app.route('/posts')
def posts():
page = int(request.args.get('page', 1))
per_page = 10
offset = (page - 1) * per_page
items = Post.query.order_by(Post.created_at.desc()) \
.offset(offset).limit(per_page).all()
if not items:
return '' # No more data — no sentinel
html = ''.join(f'<li>{p.title}</li>' for p in items)
if len(items) == per_page: # Possibly more pages
next_page = page + 1
html += f'''
<li
hx-get="/posts?page={next_page}"
hx-trigger="revealed"
hx-target="#post-list"
hx-swap="beforeend">
<span class="htmx-indicator">Loading more...</span>
</li>'''
return html
Full Page Structure
<h2>Latest Posts</h2>
<ul id="post-list">
<!-- First 10 posts rendered by server on initial page load -->
<li>First post title</li>
<li>Second post title</li>
...
<!-- Initial sentinel for page 2 -->
<li
hx-get="/posts?page=2"
hx-trigger="revealed"
hx-target="#post-list"
hx-swap="beforeend">
<span class="htmx-indicator">Loading more posts...</span>
</li>
</ul>
Showing a "You Are All Caught Up" Message
When the last page loads and the server returns no sentinel, the list simply ends. You can add a friendly end-of-list message by having the server include it with the final batch:
<!-- Server response when no more pages exist --> <li>Last post title</li> <li style="list-style:none; text-align:center; color:#888;"> You have reached the end. </li>
Infinite Scroll vs Pagination
| Feature | Infinite Scroll | Pagination |
|---|---|---|
| User control | Automatic — content loads on scroll | Manual — user clicks Next |
| Best for | Feeds, news, social content | Search results, data tables |
| Bookmarkability | Hard — no fixed URL per position | Easy — each page has its own URL |
| Performance | DOM grows large with many items | DOM stays small per page |
Handling Large DOM Growth
As the user scrolls through hundreds of items, the page DOM grows large and can slow down the browser. One mitigation is to remove old items from the top of the list as new ones appear at the bottom — a technique called a "virtual list." HTMX does not handle this automatically. For very large datasets, combine HTMX infinite scroll with a small JavaScript snippet that prunes old list items when the list exceeds a threshold.
Key Takeaway
HTMX infinite scroll uses the revealed trigger on a sentinel element to fire the next page request automatically. The server returns the next batch of items plus a new sentinel for the following page. When no more pages exist, the server omits the sentinel and the scroll stops naturally. The entire mechanism needs no custom JavaScript — just HTML attributes and a server that knows how to paginate.
