Pagination vs infinite scroll

Free preview 4 min to read

A web page with 500 records can't render them all at once. There are two solutions: pagination, which splits records into separate pages the user moves between, and infinite scroll, which loads more records as the user scrolls toward the bottom. Each pattern has trade-offs, and a well-built Rails app needs both.

Traditional pagination: Pagy

This app uses the Pagy gem for offset-based pagination in the admin area. The admin users list shows 10 users per page, with numbered page links:

# Gemfile
gem "pagy", "~> 43.3"

# config/initializers/pagy.rb
Pagy::OPTIONS[:limit] = 10

The query object clamps the page number to the pages that exist, then calculates the offset:

# app/queries/admin/identity/list_users_query.rb
def self.call(search: nil, role: nil, page: 1, per_page: Pagy::OPTIONS[:limit] || Pagy::DEFAULT[:limit])
  users = ::Identity::User.order(created_at: :desc)
  # ... search/role filters narrow `users` here ...
  total_count = users.count
  page = Admin::PageNumber.clamp(page, total_count: total_count, per_page: per_page)
  offset = (page - 1) * per_page
  paginated_users = users.limit(per_page).offset(offset)
  Result.new(users: paginated_users, total_count: total_count)
end

Page 1 shows users 1 to 10, and page 2 shows users 11 to 20. The user clicks "Next" or a page number to move, and each click is a full request that replaces the page content.

The pagination helper renders the controls:

<%# app/views/admin/identity/users/index.html.erb %>
<% pagy = admin_paginator(total_count: @total_count, per_page: @per_page) %>
<div class="mt-8 flex justify-center">
  <%= admin_pagination(pagy) if pagy.pages > 1 %>
</div>

This is the classic pattern. It works well for admin interfaces, where users need to jump to a specific page, bookmark a position or share a link to "page 3 of users." The URL encodes the position: /admin/users?page=3.

When pagination breaks down

Pagination works for admin dashboards and search results. It breaks down for content feeds (social timelines, comment threads, activity logs), where the user thinks of a continuous stream rather than a numbered grid.

The problems:

  1. Lost context: clicking "Page 2" replaces the whole page, and the user loses their scroll position and what they were reading.
  2. Meaningless page numbers: page 7 of a comment thread tells the user nothing about what's on it. Pages in a feed have no stable identity, since new content shifts everything.
  3. Interruption: the user has to stop reading, find the pagination control, click, wait for the page, then find where to continue. Every page boundary adds friction.

Infinite scroll: the continuous stream

Infinite scroll removes page boundaries. When the user scrolls near the bottom, the app fetches the next batch of records and appends them to the existing list, and the user keeps scrolling without a break.

sequenceDiagram
  participant U as User
  participant B as Browser
  participant S as Server

  U->>B: Scrolls down
  B->>B: IntersectionObserver fires
  B->>S: GET /comments?page=2 (Turbo Stream)
  S->>B: turbo_stream.append + replace load-more
  B->>B: New comments appear below existing ones
  U->>U: Continues scrolling seamlessly

This app's comment thread already uses a manual version of this pattern: a "Load older comments" button that appends comments through Turbo Streams. Infinite scroll automates the button click. Instead of the user clicking it, the browser notices that the user is near the bottom and starts the load itself.

When to use which

Pattern Best for Examples in this app
Pagination Admin lists, search results, data tables Admin users list, admin courses list
Load more button Comment threads, replies Discussion thread in lessons
Infinite scroll Activity feeds, timelines, continuous content Not yet implemented; this course builds it

The deciding factor is what the user is trying to do. Admin users want to jump to a specific page, filter and share URLs. Feed readers want to read content in order without interruption.

What this course builds

Over the next six lessons, you'll build an infinite scroll system with tools already in this app:

  • IntersectionObserver (lesson 2): the browser API that detects when an element enters the viewport, already used in this app's navbar to toggle a shadow on scroll.
  • Turbo Frames (lesson 3): how this app uses frames for paginated content in the admin area.
  • Turbo Streams append (lesson 4): how the comment thread appends new records without replacing the page, the foundation for infinite scroll.
  • The Stimulus controller (lesson 5): a reusable infinite-scroll controller that combines IntersectionObserver with Turbo Streams.
  • Edge cases (lesson 6): loading states, error handling, scroll restoration and SEO.
  • Putting it together (lesson 7): the complete pattern, from server query to rendered content.
Lesson 1 of 7 · free

Keep going with lesson 2

IntersectionObserver: the scroll detector

Explains how IntersectionObserver and a sentinel element detect when the user nears the bottom of a list.

A free account also gives you the quizzes, your progress and notes, and the discussion.

Type to start searching...