Infinite scroll without breaking the back button
Auto-Load Infinite Scroll (Progressive Enhancement)
Overview
The “Load More” button now has a stable ID (load-more-trigger) that enables progressive enhancement with auto-loading via IntersectionObserver.
Current Implementation
The button is accessible via:
<a id="load-more-trigger" href="/blog?cursor=..." data-turbo-frame="blog-posts">
Load More Posts
</a>
Progressive Enhancement: Auto-Load on Scroll
You can optionally add JavaScript to automatically load more posts when the user scrolls near the button. This is completely optional - the button works perfectly without it.
Implementation
File: app/javascript/application.js (or your main JS file)
// Auto-load infinite scroll when "Load More" button becomes visible
document.addEventListener('turbo:load', () => {
const loadMoreButton = document.getElementById('load-more-trigger');
if (!loadMoreButton) return;
// Create IntersectionObserver to watch when button enters viewport
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
// When button is visible (threshold: 10% visible)
if (entry.isIntersecting) {
// Click the button to load more posts
loadMoreButton.click();
}
});
}, {
threshold: 0.1, // Trigger when 10% of button is visible
rootMargin: '200px' // Start loading 200px before button is visible
});
// Start observing the button
observer.observe(loadMoreButton);
});
How It Works
- User scrolls down the blog posts page
- IntersectionObserver detects when “Load More” button enters viewport
- Automatically clicks the button (triggers Turbo Frame request)
- New posts load via Turbo Stream (same as manual click)
- Process repeats until all posts are loaded
Benefits
✅ Progressive Enhancement: Works without JS, enhanced with JS
✅ Better UX: No need to click “Load More” manually
✅ Stable ID: Button always has id="load-more-trigger"
✅ Turbo Compatible: Uses Turbo events (turbo:load)
✅ Performance: Only loads when user scrolls near bottom
Configuration Options
{
threshold: 0.1, // Trigger when 10% visible (0.0 to 1.0)
rootMargin: '200px' // Start loading 200px before visible
}
Adjust based on your needs:
- Higher threshold (e.g.,
0.5) = Load when button is 50% visible - Larger rootMargin (e.g.,
'500px') = Start loading earlier - Lower threshold (e.g.,
0.05) = Load when button is barely visible
Loading Indicator (Optional)
You can add a loading state:
document.addEventListener('turbo:load', () => {
const loadMoreButton = document.getElementById('load-more-trigger');
if (!loadMoreButton) return;
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Add loading state
loadMoreButton.disabled = true;
loadMoreButton.innerHTML = '<i class="bi bi-hourglass-split me-2"></i>Loading...';
// Click to load
loadMoreButton.click();
}
});
}, { threshold: 0.1, rootMargin: '200px' });
observer.observe(loadMoreButton);
// Re-enable button after Turbo Stream completes
document.addEventListener('turbo:frame-load', () => {
const button = document.getElementById('load-more-trigger');
if (button) {
button.disabled = false;
button.innerHTML = '<i class="bi bi-arrow-down-circle me-2"></i>Load More Posts';
}
});
});
Error Handling (Optional)
document.addEventListener('turbo:load', () => {
const loadMoreButton = document.getElementById('load-more-trigger');
if (!loadMoreButton) return;
let isLoading = false;
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !isLoading) {
isLoading = true;
loadMoreButton.click();
}
});
}, { threshold: 0.1, rootMargin: '200px' });
observer.observe(loadMoreButton);
// Reset loading state after frame loads
document.addEventListener('turbo:frame-load', () => {
isLoading = false;
});
// Handle errors
document.addEventListener('turbo:frame-missing', () => {
isLoading = false;
console.error('Failed to load more posts');
});
});
Browser Compatibility
IntersectionObserver is supported in:
- ✅ Chrome 51+
- ✅ Firefox 55+
- ✅ Safari 12.1+
- ✅ Edge 15+
For older browsers, you can use a polyfill or fall back to manual clicking.
Summary
The stable id="load-more-trigger" enables:
- ✅ Auto-loading via IntersectionObserver
- ✅ Progressive enhancement (works without JS)
- ✅ Future enhancements (loading states, error handling, etc.)
- ✅ Consistent behavior across page loads and Turbo Stream updates
Note: Auto-loading is optional. The infinite scroll works perfectly without JavaScript - users can manually click “Load More” if they prefer.