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 1 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 1
  2. Demo post 2
  3. Demo post 3
  4. Demo post 4
  5. Demo post 5
<%= 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 1
  2. Demo post 2
  3. Demo post 3
  4. Demo post 4
  5. Demo post 5
<%= 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 1
  2. Demo post 2
  3. Demo post 3
  4. Demo post 4
  5. Demo post 5
<%= 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 1
  2. Demo post 2
  3. Demo post 3
  4. Demo post 4
  5. Demo post 5
<%= 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"