PhiaUi.Components.Pagination (phia_ui v0.1.17)

Copy Markdown View Source

Pagination navigation component for paginating large data sets.

Built with pure HEEx and phx-click / phx-value-page for LiveView integration. No JavaScript hooks required.

Sub-components

ComponentElementPurpose
pagination/1<nav>Landmark wrapper with role="navigation"
pagination_content/1<ul>Flex row container for all page items
pagination_item/1<li>Individual item wrapper
pagination_link/1<button>Page number button with active-state highlight
pagination_previous/1<button>Previous-page button (disabled on page 1)
pagination_next/1<button>Next-page button (disabled on last page)
pagination_ellipsis/1<span> gap indicator for skipped page ranges

Basic LiveView integration

The simplest pattern: every page is rendered as a button. For small sets (≤ 7 pages) render all pages directly without ellipsis.

defmodule MyAppWeb.PostsLive do
  use MyAppWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, assign(socket, page: 1, total_pages: 10, posts: load_posts(1))}
  end

  def handle_event("go", %{"page" => page}, socket) do
    page = String.to_integer(page)
    {:noreply, assign(socket, page: page, posts: load_posts(page))}
  end
end

Template:

<.pagination>
  <.pagination_content>
    <.pagination_item>
      <.pagination_previous current_page={@page} total_pages={@total_pages} on_change="go" />
    </.pagination_item>
    <%= for p <- 1..@total_pages do %>
      <.pagination_item>
        <.pagination_link page={p} current_page={@page} on_change="go">
          {p}
        </.pagination_link>
      </.pagination_item>
    <% end %>
    <.pagination_item>
      <.pagination_next current_page={@page} total_pages={@total_pages} on_change="go" />
    </.pagination_item>
  </.pagination_content>
</.pagination>

Pagination with ellipsis (large page counts)

For large page counts, show first, last, current, and neighbours, with pagination_ellipsis/1 for gaps. Compute the visible page range in the LiveView to keep the template clean:

defp page_window(current, total) do
  neighbors = MapSet.new([1, total, current, current - 1, current + 1])
  Enum.filter(1..total, &MapSet.member?(neighbors, &1))
end

Then in the template:

<.pagination_content>
  <.pagination_item>
    <.pagination_previous current_page={@page} total_pages={@total} on_change="go" />
  </.pagination_item>
  <%= for {p, idx} <- Enum.with_index(page_window(@page, @total)) do %>
    <%!-- Insert ellipsis when there's a gap between consecutive visible pages --%>
    <%= if idx > 0 and p - Enum.at(page_window(@page, @total), idx - 1) > 1 do %>
      <.pagination_item><.pagination_ellipsis /></.pagination_item>
    <% end %>
    <.pagination_item>
      <.pagination_link page={p} current_page={@page} on_change="go">{p}</.pagination_link>
    </.pagination_item>
  <% end %>
  <.pagination_item>
    <.pagination_next current_page={@page} total_pages={@total} on_change="go" />
  </.pagination_item>
</.pagination_content>

URL-based pagination with handle_params

For shareable URLs use phx-patch or update params in handle_event:

def handle_event("go", %{"page" => page}, socket) do
  {:noreply, push_patch(socket, to: ~p"/posts?page=#{page}")}
end

def handle_params(%{"page" => page}, _uri, socket) do
  page = String.to_integer(page)
  {:noreply, assign(socket, page: page, posts: load_posts(page))}
end

Accessibility

  • The <nav> carries role="navigation" and aria-label="pagination".
  • The active page button carries aria-current="page".
  • Previous/next buttons carry descriptive aria-label attributes.
  • Disabled prev/next buttons set the HTML disabled attribute so keyboard navigation and screen readers skip them.

Summary

Functions

Renders cursor-based pagination with Previous and Next buttons.

Renders a centered "Load more" button for append-style infinite scroll.

Renders the <nav role="navigation" aria-label="pagination"> wrapper.

Renders the <ul> flex container for pagination items.

Renders a ellipsis to indicate a gap in the page number sequence.

Renders a <li> wrapper for a single pagination control.

Renders a page number button.

Renders a next-page button with a chevron-right icon.

Renders a previous-page button with a chevron-left icon.

Functions

cursor_pagination(assigns)

Renders cursor-based pagination with Previous and Next buttons.

Unlike offset pagination, cursor pagination uses opaque cursor tokens rather than page numbers. Useful for infinite-scroll APIs and large result sets where offset pagination is expensive.

Attributes

  • has_previous_page (:boolean) (required) - Whether a previous page exists.
  • has_next_page (:boolean) (required) - Whether a next page exists.
  • on_previous (:string) - phx-click event for previous. Defaults to "previous-page".
  • on_next (:string) - phx-click event for next. Defaults to "next-page".
  • previous_cursor (:any) - Cursor value sent with previous event. Defaults to nil.
  • next_cursor (:any) - Cursor value sent with next event. Defaults to nil.
  • class (:string) - Defaults to nil.
  • Global attributes are accepted.

load_more(assigns)

Renders a centered "Load more" button for append-style infinite scroll.

When loading is true, the button shows a spinner icon and the loading_label text and is non-interactive.

Attributes

  • loading (:boolean) - Shows spinner and loading_label when true. Defaults to false.
  • on_click (:string) (required) - phx-click event to load the next batch.
  • label (:string) - Defaults to "Load more".
  • loading_label (:string) - Defaults to "Loading...".
  • disabled (:boolean) - Defaults to false.
  • class (:string) - Defaults to nil.
  • Global attributes are accepted.

pagination(assigns)

Renders the <nav role="navigation" aria-label="pagination"> wrapper.

This is the outermost container. Always place a pagination_content/1 inside it.

Attributes

  • class (:string) - Additional CSS classes applied to the <nav> element. Defaults to nil.
  • Global attributes are accepted. HTML attributes forwarded to the <nav> element.

Slots

pagination_content(assigns)

Renders the <ul> flex container for pagination items.

All pagination_item/1 children are rendered in a horizontal flex row with a small gap between items.

Attributes

  • class (:string) - Additional CSS classes applied to the <ul> element. Defaults to nil.
  • Global attributes are accepted. HTML attributes forwarded to the <ul> element.

Slots

pagination_ellipsis(assigns)

Renders a ellipsis to indicate a gap in the page number sequence.

Use between pagination_link/1 items when collapsing a range of pages. The ellipsis is aria-hidden="true" because it carries no actionable information for screen readers — the surrounding visible page numbers provide sufficient context.

Example

<%!-- Renders: 1  4 5 6  20 --%>
<.pagination_item><.pagination_link page={1} ...>1</.pagination_link></.pagination_item>
<.pagination_item><.pagination_ellipsis /></.pagination_item>
<.pagination_item><.pagination_link page={4} ...>4</.pagination_link></.pagination_item>
...

Attributes

  • class (:string) - Additional CSS classes applied to the <span> element. Defaults to nil.
  • Global attributes are accepted. HTML attributes forwarded to the <span> element.

pagination_item(assigns)

Renders a <li> wrapper for a single pagination control.

Wrap each pagination_link/1, pagination_previous/1, pagination_next/1, and pagination_ellipsis/1 in a pagination_item/1.

Attributes

  • class (:string) - Additional CSS classes applied to the <li> element. Defaults to nil.
  • Global attributes are accepted. HTML attributes forwarded to the <li> element.

Slots

pagination_link(assigns)

Renders a page number button.

The active page (page == current_page) is highlighted with bg-primary text-primary-foreground and receives aria-current="page". All other pages use a transparent background with a hover accent.

Fires a phx-click event with phx-value-page={page} when clicked. The LiveView receives the page as a string, so cast with String.to_integer/1 in handle_event.

Example

<.pagination_link page={3} current_page={@page} on_change="navigate">
  3
</.pagination_link>

Attributes

  • page (:integer) (required) - The page number this button represents (1-based).
  • current_page (:integer) (required) - The currently active page number. Controls active styling and aria-current.
  • on_change (:string) - The phx-click event name sent to the LiveView. Receives page as a string value via phx-value-page. Defaults to "page-changed".
  • class (:string) - Additional CSS classes applied to the <button> element. Defaults to nil.
  • Global attributes are accepted. HTML attributes forwarded to the <button> element.

Slots

  • inner_block (required) - Page label — typically just the page number.

pagination_next(assigns)

Renders a next-page button with a chevron-right icon.

When current_page == total_pages, the button is visually and functionally disabled:

  • The HTML disabled attribute is set.
  • pointer-events-none opacity-50 is applied.
  • phx-click is not attached.

Example

<.pagination_next current_page={@page} total_pages={@total} on_change="go" />

Attributes

  • current_page (:integer) (required) - The currently active page. When equal to total_pages, the button is disabled.
  • total_pages (:integer) (required) - Total number of pages. Used to disable the button on the last page.
  • on_change (:string) - The phx-click event name sent to the LiveView. Defaults to "page-changed".
  • class (:string) - Additional CSS classes applied to the <button> element. Defaults to nil.
  • Global attributes are accepted. HTML attributes forwarded to the <button> element.

pagination_previous(assigns)

Renders a previous-page button with a chevron-left icon.

When current_page == 1, the button is visually and functionally disabled:

  • The HTML disabled attribute is set (removes from tab order).
  • pointer-events-none opacity-50 is applied.
  • phx-click is not attached (no event fires).

Example

<.pagination_previous current_page={@page} total_pages={@total} on_change="go" />

Attributes

  • current_page (:integer) (required) - The currently active page. When 1, the button is disabled.
  • total_pages (:integer) (required) - Total number of pages. Used to determine the page value sent on click.
  • on_change (:string) - The phx-click event name sent to the LiveView. Defaults to "page-changed".
  • class (:string) - Additional CSS classes applied to the <button> element. Defaults to nil.
  • Global attributes are accepted. HTML attributes forwarded to the <button> element.