import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["sentinel"]
static values = {
url: String
}
connect() {
this.observer = new IntersectionObserver(
entries => this.handleIntersection(entries),
{ rootMargin: "100px" }
)
if (this.hasSentinelTarget) {
this.observer.observe(this.sentinelTarget)
}
}
disconnect() {
this.observer.disconnect()
}
handleIntersection(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Trigger the frame to load
this.sentinelTarget.src = this.urlValue
this.observer.unobserve(this.sentinelTarget)
}
})
}
}
<div class="posts-list">
<h1>Recent Posts</h1>
<div id="posts" class="space-y-6">
<%= render @posts %>
<% if @posts.next_page %>
<%= turbo_frame_tag "page_#{@posts.next_page}",
loading: :lazy,
data: {
controller: "infinite-scroll",
infinite_scroll_target: "sentinel",
infinite_scroll_url_value: posts_path(page: @posts.next_page)
} do %>
<div class="text-center py-8">
<div class="spinner"></div>
<p class="text-gray-500">Loading more posts...</p>
</div>
<% end %>
<% end %>
</div>
</div>
class PostsController < ApplicationController
def index
@posts = Post.published
.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(20)
respond_to do |format|
format.html
format.turbo_stream do
render turbo_stream: turbo_stream.append(
"posts",
partial: "posts/list",
locals: { posts: @posts }
)
end
end
end
end
Infinite scroll improves perceived performance by loading content as users reach the bottom of the page. I combine Turbo Frames with the Intersection Observer API via a Stimulus controller. The last item in each page has a sentinel element that, when visible, triggers a frame load for the next page. The server returns the next page wrapped in the same frame ID, which replaces the sentinel with new content plus a new sentinel. This approach is more efficient than scroll event listeners and works seamlessly with Turbo's caching. I also provide a fallback 'Load More' button for users with JavaScript disabled or those who prefer manual control.