PagyInfiniteScroll
A Rails gem that adds infinite scroll functionality using Pagy and Stimulus. Load initial records and automatically fetch more as users scroll down - perfect for optimizing pages with large datasets.
Features
- 🚀 Easy integration with existing Rails apps
- 📦 Built on Pagy (fast, lightweight pagination)
- ⚡ Stimulus controller for smooth infinite scrolling
- 🎨 Customizable HTML rendering
- 🔧 Configurable scroll threshold and items per page
- 💾 Preserves URL parameters during scroll
Installation
Add to your Gemfile:
gem 'pagy_infinite_scroll', '~> 0.1.1'Then run:
bundle install
rails generate pagy_infinite_scroll:installThe generator will automatically detect your JavaScript setup and configure accordingly:
For importmap-rails (auto-configured ✅)
The generator will:
- Add pin to
config/importmap.rb - Add import to
app/javascript/application.js - Auto-register controller with Stimulus
- No manual steps needed!
For jsbundling-rails (esbuild/webpack/rollup)
The generator will:
- Create
config/initializers/pagy_infinite_scroll.rb - Copy controller to
app/javascript/controllers/pagy_infinite_scroll_controller.js - Display registration instructions
- Run
yarn buildafter setup
Quick Start
1. Controller Setup
Use the gem's helper methods:
class ProductsController < ApplicationController
def index
# Use pagy_infinite_scroll instead of regular pagy
@pagy, @products = pagy_infinite_scroll(Product.all, limit: 50)
respond_to do |format|
format.html
format.json do
# Use the JSON helper to format response
render json: pagy_infinite_scroll_json(@pagy, @products) { |product|
{
id: product.id,
title: product.title,
price: product.price
}
}
end
end
end
end2. View Setup
Add the infinite scroll container to your view:
<div data-controller="pagy-infinite-scroll"
data-pagy-infinite-scroll-url-value="<%= products_path(format: :json) %>"
data-pagy-infinite-scroll-page-value="1"
data-pagy-infinite-scroll-has-more-value="<%= @pagy.next.present? %>"
style="max-height: 600px; overflow-y: auto;">
<!-- Container for items -->
<div data-pagy-infinite-scroll-target="itemsContainer">
<% @products.each do |product| %>
<div class="product-card">
<h3><%= product.title %></h3>
<p><%= product.price %></p>
</div>
<% end %>
</div>
<!-- Loading indicator -->
<div data-pagy-infinite-scroll-target="loadingIndicator" class="hidden">
<p>Loading more...</p>
</div>
</div>⚠️ Important: Custom HTML Rendering
The gem provides the core infinite scroll functionality, but you need to tell it how to render YOUR specific HTML.
Why?
The gem cannot know:
- Your specific HTML structure (forms, checkboxes, badges, etc.)
- Your CSS classes and styling
- Your form field names
- Your Stimulus action targets
Solution: Extend the Controller
For jsbundling-rails apps:
Create a custom Stimulus controller that extends the gem's base controller:
// app/javascript/controllers/products_scroll_controller.js
import PagyInfiniteScrollController from "./pagy_infinite_scroll_controller"
export default class extends PagyInfiniteScrollController {
// Override this method to customize HTML for loaded items
createItemHTML(record) {
return `
<div class="product-card">
<h3>${record.title}</h3>
<p class="price">$${record.price}</p>
<span class="badge">${record.category}</span>
</div>
`
}
}Register it in app/javascript/controllers/index.js:
import ProductsScrollController from "./products_scroll_controller"
application.register("products-scroll", ProductsScrollController)For importmap-rails apps:
The controller is available globally as window.PagyInfiniteScrollController:
// app/javascript/controllers/products_scroll_controller.js
class ProductsScrollController extends window.PagyInfiniteScrollController {
createItemHTML(record) {
return `
<div class="product-card">
<h3>${record.title}</h3>
<p class="price">$${record.price}</p>
<span class="badge">${record.category}</span>
</div>
`
}
}
// Register with Stimulus
Stimulus.register("products-scroll", ProductsScrollController)Use in your view:
<div data-controller="products-scroll"
data-products-scroll-url-value="<%= products_path(format: :json) %>"
data-products-scroll-page-value="1"
data-products-scroll-has-more-value="<%= @pagy.next.present? %>">
...
</div>Real-World Example: Form with Checkboxes
// app/javascript/controllers/distribution_preferences_scroll_controller.js
import PagyInfiniteScrollController from "./pagy_infinite_scroll_controller"
export default class extends PagyInfiniteScrollController {
createItemHTML(record) {
const isChecked = record.selected ? 'checked' : ''
return `
<div class="flex items-center gap-4 p-4">
<input type="checkbox"
name="product_ids[]"
value="${record.id}"
id="product_${record.id}"
${isChecked}
class="form-checkbox">
<label for="product_${record.id}" class="flex-1">
<div class="font-medium">${record.title}</div>
${record.vendor_name ? `
<span class="badge">${record.vendor_name}</span>
` : ''}
<span class="text-gray-500">
${record.variants_count} variants
</span>
</label>
</div>
`
}
}Key Points:
- Only ~60 lines of custom JavaScript needed
- The gem handles all the scroll detection, AJAX, and state management
- You only define the HTML structure for your specific use case
- This keeps your code DRY and maintainable
Configuration
Edit config/initializers/pagy_infinite_scroll.rb:
PagyInfiniteScroll.configure do |config|
config.items_per_page = 50 # Items per page (default: 25)
config.scroll_threshold = 100 # Pixels from bottom to trigger load (default: 100)
config.loading_indicator = true # Show loading indicator (default: true)
config.preserve_state = true # Preserve URL params (default: true)
config.debounce_delay = 500 # Debounce for search in ms (default: 500)
endData Attributes Reference
Container Attributes
-
data-pagy-infinite-scroll-url-value- JSON endpoint URL (required) -
data-pagy-infinite-scroll-page-value- Current page number (default: 1) -
data-pagy-infinite-scroll-has-more-value- Has more pages? (required, true/false) -
data-pagy-infinite-scroll-threshold-value- Scroll threshold in pixels (optional)
Targets
-
data-pagy-infinite-scroll-target="itemsContainer"- Where to append items -
data-pagy-infinite-scroll-target="loadingIndicator"- Loading indicator element
Events
The controller dispatches custom events you can listen to:
// In your custom controller
connect() {
super.connect()
this.element.addEventListener('pagy-infinite-scroll:loaded', (event) => {
console.log('Loaded page:', event.detail.page)
console.log('Has more:', event.detail.hasMore)
console.log('Count:', event.detail.count)
})
this.element.addEventListener('pagy-infinite-scroll:error', (event) => {
console.error('Error:', event.detail.error)
})
}Troubleshooting
Items not loading on scroll
- Check browser console for JavaScript errors
-
Verify JSON endpoint returns correct format:
{ "records": [...], "pagy": { "page": 1, "pages": 10, "next": 2, "count": 500 } } - Check Network tab to see if AJAX requests are being made
- Verify controller is connected - should see console log on page load
Controller not found error
Run yarn build (or npm run build) to rebuild JavaScript bundle.
Helpers not available in controller
Restart your Rails server after installing the gem.
Items render as JSON instead of HTML
You need to create a custom controller that extends the base controller and overrides the createItemHTML() method (see "Custom HTML Rendering" section above).
JavaScript Setup: importmap vs jsbundling
This gem works with both importmap-rails and jsbundling-rails:
importmap-rails ✅
- Setup: Automatic via generator
- Controller: Standalone file, auto-registers with Stimulus
-
Extending: Use
window.PagyInfiniteScrollControlleras base class - Advantages: Zero build step, simpler setup
- File location: Served from gem's assets
jsbundling-rails (esbuild/webpack/rollup) ✅
- Setup: Manual file copy (bundlers can't access gem paths)
-
Controller: Import from
./pagy_infinite_scroll_controller -
Extending: Use ES6
importandextends - Advantages: Full ES6 module support, tree-shaking
-
File location:
app/javascript/controllers/
Both setups support the same features and API!
Requirements
- Rails 7.0+
- Pagy gem (add
gem 'pagy'to your Gemfile) - Stimulus (Hotwire)
- Either importmap-rails OR jsbundling-rails (esbuild, webpack, or rollup)
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/hassanharoon86/pagy_infinite_scroll/issues
License
The gem is available as open source under the terms of the MIT License.