Pagination

Pagination with page navigation, next and previous links.

No JavaScript Required
pagination/default
<%= render(Shadcn::PaginationComponent.new) do |pagination| %>
  <% pagination.with_pagination_content do |content| %>
    <% content.with_previous(href: "#") %>
    <% content.with_item(href: "#") { "1" } %>
    <% content.with_item(href: "#", active: true) { "2" } %>
    <% content.with_item(href: "#") { "3" } %>
    <% content.with_next_page(href: "#") %>
  <% end %>
<% end %>

Installation

Add the component to your project:

rails generate shadcn:add pagination

Usage

There are three ways to use the Pagination component.

1. Slot-based API (Full Control)

Use slots when you want to decide every link and ellipsis yourself.

<%= render Shadcn::PaginationComponent.new do |pagination| %>
  <% pagination.with_pagination_content do |content| %>
    <% content.with_previous(href: posts_path(page: 1)) %>
    <% content.with_item(href: posts_path(page: 1)) { "1" } %>
    <% content.with_item(href: posts_path(page: 2), active: true) { "2" } %>
    <% content.with_item(href: posts_path(page: 3)) { "3" } %>
    <% content.with_next_page(href: posts_path(page: 3)) %>
  <% end %>
<% end %>

2. Collection-based API (Kaminari/will_paginate)

Pass a paginated collection that exposes Kaminari or will_paginate pagination methods.

<%= turbo_frame_tag "results" do %>
  <%= render @posts %>

  <%= render Shadcn::Pagination.new(
    collection: @posts,
    url_builder: ->(page) { posts_path(page: page, anchor: "results") }
  ) %>
<% end %>

<%# Drop-in replacement for Kaminari's paginate helper: %>
<%= shadcn_paginate @posts, url_builder: ->(page) { posts_path(page: page, anchor: "results") } %>

For a drop-in Kaminari view replacement, keep Kaminari in your controller and replace <%= paginate @posts %> with shadcn_paginate.

<%= turbo_frame_tag "results" do %>
  <%= render @posts %>

  <%= shadcn_paginate @posts, url_builder: ->(page) { posts_path(page: page, anchor: "results") } %>
<% end %>

3. Pagy-based API

Pass a Pagy object, or a compatible object that responds to page, pages, prev, and next.

<%= turbo_frame_tag "results" do %>
  <%= render @posts %>

  <%= render Shadcn::Pagination.new(
    pagy: @pagy,
    url_builder: ->(page) { posts_path(page: page, anchor: "results") }
  ) %>
<% end %>

<%# Helper form: %>
<%= shadcn_paginate @pagy, url_builder: ->(page) { posts_path(page: page, anchor: "results") } %>

Examples

Basic

<%= render Shadcn::PaginationComponent.new do |pagination| %>
  <% pagination.with_pagination_content do |content| %>
    <% content.with_previous(href: posts_path(page: 1)) %>
    <% content.with_item(href: posts_path(page: 1)) { "1" } %>
    <% content.with_item(href: posts_path(page: 2), active: true) { "2" } %>
    <% content.with_item(href: posts_path(page: 3)) { "3" } %>
    <% content.with_next_page(href: posts_path(page: 3)) %>
  <% end %>
<% end %>

With Ellipsis

<%= render Shadcn::PaginationComponent.new do |pagination| %>
  <% pagination.with_pagination_content do |content| %>
    <% content.with_previous(href: posts_path(page: 4)) %>
    <% content.with_item(href: posts_path(page: 1)) { "1" } %>
    <% content.with_ellipse %>
    <% content.with_item(href: posts_path(page: 4)) { "4" } %>
    <% content.with_item(href: posts_path(page: 5), active: true) { "5" } %>
    <% content.with_item(href: posts_path(page: 6)) { "6" } %>
    <% content.with_ellipse %>
    <% content.with_item(href: posts_path(page: 10)) { "10" } %>
    <% content.with_next_page(href: posts_path(page: 6)) %>
  <% end %>
<% end %>

Disabled Navigation

On the first or last page, disable the previous/next buttons:

<%= render Shadcn::PaginationComponent.new do |pagination| %>
  <% pagination.with_pagination_content do |content| %>
    <% content.with_previous(disabled: true) %>
    <% content.with_item(href: posts_path(page: 1), active: true) { "1" } %>
    <% content.with_item(href: posts_path(page: 2)) { "2" } %>
    <% content.with_item(href: posts_path(page: 3)) { "3" } %>
    <% content.with_next_page(href: posts_path(page: 2)) %>
  <% end %>
<% end %>
pagination/with_pagy
<%= render Shadcn::Pagination.new(
  pagy: @pagy,
  url_builder: ->(page) { posts_path(page: page) }
) %>
pagination/with_kaminari_collection
<%= render Shadcn::Pagination.new(
  collection: @posts,
  url_builder: ->(page) { posts_path(page: page) }
) %>

Gem Integration

The Pagination component accepts real pagination objects from Pagy, Kaminari, and will_paginate. Each live demo below builds the matching gem object from the same 50-post list and passes that object directly to the component.

Demo data: showing page 5 of 10 for a fake 50-post list.

For live Rails pages, build pager URLs with an anchor such as posts_path(page: page, anchor: "results"). If the list should update in place, wrap the list and pager in an optional turbo_frame_tag "results".

Pagy

Pagy objects expose page, pages, prev, and next. Include Pagy::Backend in your controller, paginate your records there, then pass the real Pagy object with pagy:.

Controller

class PostsController < ApplicationController
  include Pagy::Backend

  def index
    @pagy, @posts = pagy(Post.all, page: params[:page], limit: 5)
  end
end

View

<%= turbo_frame_tag "results" do %>
  <%= render @posts %>

  <%= render Shadcn::Pagination.new(
    pagy: @pagy,
    url_builder: ->(page) { posts_path(page: page, anchor: "results") }
  ) %>
<% end %>

<%# Helper form: %>
<%= shadcn_paginate @pagy, url_builder: ->(page) { posts_path(page: page, anchor: "results") } %>
  1. Demo post 21
  2. Demo post 22
  3. Demo post 23
  4. Demo post 24
  5. Demo post 25
<%= turbo_frame_tag "results" do %>
  <%= render @posts %>

  <%= render Shadcn::Pagination.new(
    pagy: @pagy,
    url_builder: ->(page) { posts_path(page: page, anchor: "results") }
  ) %>
<% end %>

<%# Helper form: %>
<%= shadcn_paginate @pagy, url_builder: ->(page) { posts_path(page: page, anchor: "results") } %>

Kaminari

Kaminari stays in your controller: use Post.page(params[:page]) for ActiveRecord models, or Kaminari.paginate_array(...).page(params[:page]).per(5) for arrays. In the view, replace Kaminari's <%= paginate @posts %> helper with shadcn_paginate or render Shadcn::Pagination directly. This changes the rendered UI, not Kaminari's theme engine.

Controller

class PostsController < ApplicationController
  def index
    @posts = Post.page(params[:page])

    # For non-ActiveRecord arrays:
    # @posts = Kaminari.paginate_array(fake_posts).page(params[:page]).per(5)
  end
end

View

<%= turbo_frame_tag "results" do %>
  <%= render @posts %>

  <%= shadcn_paginate @posts, url_builder: ->(page) { posts_path(page: page, anchor: "results") } %>
<% end %>
  1. Demo post 21
  2. Demo post 22
  3. Demo post 23
  4. Demo post 24
  5. Demo post 25
<%= turbo_frame_tag "results" do %>
  <%= render @posts %>

  <%= render Shadcn::Pagination.new(
    collection: @posts,
    url_builder: ->(page) { posts_path(page: page, anchor: "results") }
  ) %>
<% end %>

<%# Drop-in replacement for Kaminari's paginate helper: %>
<%= shadcn_paginate @posts, url_builder: ->(page) { posts_path(page: page, anchor: "results") } %>

will_paginate

will_paginate collections expose current_page, total_pages, previous_page, and next_page. Use Post.paginate(page: params[:page]) in your controller, then pass the real will_paginate collection with the same collection: option.

Controller

class PostsController < ApplicationController
  def index
    @posts = Post.paginate(page: params[:page], per_page: 5)
  end
end

View

<%= turbo_frame_tag "results" do %>
  <%= render @posts %>

  <%= render Shadcn::Pagination.new(
    collection: @posts,
    url_builder: ->(page) { posts_path(page: page, anchor: "results") }
  ) %>
<% end %>

<%# Helper form: %>
<%= shadcn_paginate @posts, url_builder: ->(page) { posts_path(page: page, anchor: "results") } %>
  1. Demo post 21
  2. Demo post 22
  3. Demo post 23
  4. Demo post 24
  5. Demo post 25
<%= turbo_frame_tag "results" do %>
  <%= render @posts %>

  <%= render Shadcn::Pagination.new(
    collection: @posts,
    url_builder: ->(page) { posts_path(page: page, anchor: "results") }
  ) %>
<% end %>

<%# Helper form: %>
<%= shadcn_paginate @posts, url_builder: ->(page) { posts_path(page: page, anchor: "results") } %>

Custom URL Builder

The url_builder lambda receives the page number and should return the URL for that page. Use it to preserve filters, sorting, anchors, or nested resource paths.

  1. Demo post 21
  2. Demo post 22
  3. Demo post 23
  4. Demo post 24
  5. Demo post 25
<%= render Shadcn::PaginationComponent.new(
  collection: @posts,
  url_builder: ->(page) {
    posts_path(page: page, status: params[:status], sort: params[:sort], anchor: "results")
  }
) %>

Window Size

Control how many pages are shown around the current page with the window option.

<%= render Shadcn::PaginationComponent.new(
  collection: @posts,
  window: 1,
  url_builder: ->(page) { posts_path(page: page, anchor: "results") }
) %>

API Reference

PaginationComponent

Prop Type Default Description
collection Object nil Kaminari or will_paginate collection
pagy Pagy nil Pagy pagination object
url_builder Proc ->(page) { "?page=#{page}" } Lambda to generate page URLs, receives page number
window Integer 2 Number of pages to show around current page

PaginationContentComponent Slots

Slot Props Description
with_previous href:, disabled: Previous page link
with_next_page href:, disabled: Next page link
with_item href:, active: Page number link (yields page number)
with_ellipse - Ellipsis indicator for skipped pages

Accessibility

  • Uses <nav> element with role="navigation"
  • Has aria-label="pagination" for screen readers
  • Current page marked with aria-current="page"
  • Disabled links have aria-disabled="true"