# `PhiaUi.Components.Pagination`
[🔗](https://github.com/charlenopires/PhiaUI/blob/v0.1.17/lib/phia_ui/components/navigation/pagination.ex#L1)

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

| Component               | Element    | Purpose                                          |
|-------------------------|------------|--------------------------------------------------|
| `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.

# `cursor_pagination`

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`

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`

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

* `inner_block` (required) - Should contain a single `pagination_content/1`.

# `pagination_content`

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

* `inner_block` (required) - Should contain `pagination_item/1` children.

# `pagination_ellipsis`

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`

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

* `inner_block` (required) - A `pagination_link/1`, `pagination_previous/1`, `pagination_next/1`, or `pagination_ellipsis/1`.

# `pagination_link`

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`

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`

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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
