Pagination
Composable, link-based pagination (server-driven). Static, no JS.
Installation
bin/rails g wabi:add paginationExample
render Components::UI::Pagination.new do
render Components::UI::PaginationContent.new do
render Components::UI::PaginationItem.new do
render Components::UI::PaginationPrevious.new(href: "#")
end
render Components::UI::PaginationItem.new do
render Components::UI::PaginationLink.new(href: "#") { "1" }
end
render Components::UI::PaginationItem.new do
render Components::UI::PaginationLink.new(href: "#", active: true) { "2" }
end
render Components::UI::PaginationItem.new do
render Components::UI::PaginationLink.new(href: "#") { "3" }
end
render Components::UI::PaginationItem.new do
render Components::UI::PaginationEllipsis.new
end
render Components::UI::PaginationItem.new do
render Components::UI::PaginationNext.new(href: "#")
end
end
end
Source
app/components/ui/pagination.rb
# frozen_string_literal: true
module Components
module UI
class Pagination < Wabi::Base
variants do
base "mx-auto flex w-full justify-center"
end
def initialize(**attrs) = @attrs = attrs
def view_template(&)
user_class = @attrs.delete(:class)
nav(role: "navigation", "aria-label": "pagination",
**@attrs, class: merge_class(tokens, user_class), &)
end
end
end
end
app/components/ui/pagination_content.rb
# frozen_string_literal: true
module Components
module UI
class PaginationContent < Wabi::Base
variants do
base "flex flex-row items-center gap-1"
end
def initialize(**attrs) = @attrs = attrs
def view_template(&)
user_class = @attrs.delete(:class)
ul(**@attrs, class: merge_class(tokens, user_class), &)
end
end
end
end
app/components/ui/pagination_item.rb
# frozen_string_literal: true
module Components
module UI
class PaginationItem < Wabi::Base
def initialize(**attrs) = @attrs = attrs
def view_template(&)
user_class = @attrs.delete(:class)
li(**@attrs, class: user_class, &)
end
end
end
end
app/components/ui/pagination_link.rb
# frozen_string_literal: true
module Components
module UI
class PaginationLink < Wabi::Base
variants do
base "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring h-10 w-10"
variant :state, {
active: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
inactive: "hover:bg-accent hover:text-accent-foreground",
}, default: :inactive
end
def initialize(active: false, **attrs)
@active = active
@attrs = attrs
end
def view_template(&)
user_class = @attrs.delete(:class)
state = @active ? :active : :inactive
a(
**(@active ? { "aria-current": "page" } : {}),
**@attrs,
class: merge_class(tokens(state: state), user_class),
&
)
end
end
end
end
app/components/ui/pagination_previous.rb
# frozen_string_literal: true
require_relative "pagination_link"
module Components
module UI
class PaginationPrevious < Wabi::Base
def initialize(**attrs) = @attrs = attrs
def view_template
user_class = @attrs.delete(:class)
render Components::UI::PaginationLink.new(
"aria-label": "Go to previous page",
**@attrs,
class: merge_class("gap-1 w-auto pl-2.5", user_class)
) do
raw(safe('<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>'))
span { "Previous" }
end
end
end
end
end
app/components/ui/pagination_next.rb
# frozen_string_literal: true
require_relative "pagination_link"
module Components
module UI
class PaginationNext < Wabi::Base
def initialize(**attrs) = @attrs = attrs
def view_template
user_class = @attrs.delete(:class)
render Components::UI::PaginationLink.new(
"aria-label": "Go to next page",
**@attrs,
class: merge_class("gap-1 w-auto pr-2.5", user_class)
) do
span { "Next" }
raw(safe('<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>'))
end
end
end
end
end
app/components/ui/pagination_ellipsis.rb
# frozen_string_literal: true
module Components
module UI
class PaginationEllipsis < Wabi::Base
variants do
base "flex h-9 w-9 items-center justify-center"
end
def initialize(**attrs) = @attrs = attrs
def view_template
user_class = @attrs.delete(:class)
# aria-hidden lives on the decorative SVG only — NOT the wrapper, or it
# would also hide the sr-only "More pages" text from assistive tech.
span(**@attrs, class: merge_class(tokens, user_class)) do
raw(safe('<svg aria-hidden="true" focusable="false" class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="1.5"/><circle cx="19" cy="12" r="1.5"/><circle cx="5" cy="12" r="1.5"/></svg>'))
span(class: "sr-only") { "More pages" }
end
end
end
end
end
Accessibility
- Pagination renders a <nav> with aria-label="pagination" so screen readers identify it as navigation.
- PaginationPrevious and PaginationNext include screen-reader-only text via sr-only spans.
- PaginationEllipsis includes a sr-only span announcing "More pages".
- Active page link receives aria-current="page" via the active: prop.