ComponentsThemes
Palette
Default
Stone
Rose
Blue
Green
Violet
Yellow
Orange
6

← Components

Pagination

Composable, link-based pagination (server-driven). Static, no JS.

Installation

bin/rails g wabi:add pagination

Example

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
# 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.