Pagination
Pagination with page navigation, next and previous links.
<%= 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 %>
<%= render Shadcn::Pagination.new(
pagy: @pagy,
url_builder: ->(page) { posts_path(page: page) }
) %>
<%= 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") } %>
- Demo post 1
- Demo post 2
- Demo post 3
- Demo post 4
- 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 %>
- Demo post 1
- Demo post 2
- Demo post 3
- Demo post 4
- 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") } %>
- Demo post 1
- Demo post 2
- Demo post 3
- Demo post 4
- 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.
- Demo post 1
- Demo post 2
- Demo post 3
- Demo post 4
- 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 withrole="navigation" - Has
aria-label="pagination"for screen readers - Current page marked with
aria-current="page" - Disabled links have
aria-disabled="true"
On This Page