Files
Htmx/docs/Components/Pagination.md
T
2026-05-04 19:57:48 +05:00

5.6 KiB
Raw Blame History

Pagination

A page navigation row with Prev/Next links and numbered page buttons. The current page is highlighted. Links are built from a URL pattern.


HTML structure

nav[aria-label=Pagination].flex.items-center.justify-center.gap-1
  a.pag-btn[href=prevUrl, aria-label=Previous]       ← disabled styling when current=1
    svg (chevron-left)
  a.pag-btn[href=url]                                 ← one per page in the visible window
    {pageNumber}                                      ← current page has pag-btn-active class
  span.pag-ellipsis                                   ← rendered when pages are skipped
  a.pag-btn[href=nextUrl, aria-label=Next]            ← disabled styling when current=total
    svg (chevron-right)

CSS mechanics

Class Effect
pag-btn inline-flex h-9 w-9 items-center justify-center rounded-md border border-input bg-background text-sm hover:bg-accent
pag-btn-active bg-primary text-primary-foreground border-primary hover:bg-primary/90
pag-ellipsis inline-flex h-9 w-9 items-center justify-center text-sm text-muted-foreground
pointer-events-none opacity-50 Applied to Prev when current == 1, to Next when current == total

The visible page window is limited to 7 buttons maximum. For large page counts the component collapses interior pages into ellipsis spans, keeping first page, last page, and the pages immediately around current always visible.


Constructor signature

public Pagination(
    int    current,
    int    total,
    string urlPattern)
Parameter Description
current 1-based current page number
total Total number of pages
urlPattern URL template with {0} replaced by the page number, e.g. "/items?page={0}"

Usage examples

Basic pagination

new Pagination(current: 3, total: 10, urlPattern: "/blog?page={0}")

Preserving query parameters

// Build the URL pattern from the current request
var query = HttpUtility.ParseQueryString(Request.QueryString.ToString());
query["page"] = "{0}";
var pattern = "/search?" + query.ToString();

new Pagination(current: page, total: totalPages, urlPattern: pattern)

HTMX-powered pagination (swap content without full navigation)

The links are standard <a> tags. To intercept them with HTMX, use hx-boost on the container or wrap in a boosted <div>:

<div hx-boost="true" hx-target="#item-list" hx-push-url="true">
  $$Pagination$$
</div>

Single page (hides naturally)

// When total == 1, Prev and Next are both disabled and only "1" is rendered.
new Pagination(current: 1, total: 1, urlPattern: "/items?page={0}")

Tips and tricks

  • The urlPattern uses string.Format-style {0} — do not use {page} or other named placeholders.
  • Page numbers are 1-based throughout — the first page is page 1.
  • When total is 0 or negative the component renders nothing — guard total > 1 in the page if you want to hide it entirely when there is only one page.
  • To preserve sort order or filters across pages, include those values in the urlPattern query string.
  • For very small page counts (23 pages), all page buttons are shown with no ellipsis.
  • For very small page counts (23 pages), all page buttons are shown with no ellipsis.

Complete page example

Templates/BlogPage.htmx

<div class="max-w-3xl mx-auto py-10">
  <h1 class="text-2xl font-bold mb-6">Blog</h1>
  <div class="space-y-6 mb-10">
    $$PostList$$
  </div>
  $$Pagination$$
</div>

Templates/BlogPage.htmx.cs

namespace Htmx.ApiDemo.Templates;

public sealed class BlogPage : BlogPageBase
{
    private readonly byte[] _postList;
    private readonly IHtmxComponent _pagination;

    public BlogPage(IEnumerable<BlogPost> posts, int currentPage, int totalPages)
    {
        var sb = new System.Text.StringBuilder();
        foreach (var post in posts)
        {
            sb.Append($"""
                <article class="border-b pb-6">
                  <h2 class="text-lg font-semibold mb-1">
                    <a href="/blog/{System.Net.WebUtility.HtmlEncode(post.Slug)}"
                       class="hover:underline">
                      {System.Net.WebUtility.HtmlEncode(post.Title)}
                    </a>
                  </h2>
                  <p class="text-sm text-muted-foreground mb-2">{post.PublishedAt:MMMM d, yyyy}</p>
                  <p class="text-sm">{System.Net.WebUtility.HtmlEncode(post.Summary)}</p>
                </article>
            """);
        }
        _postList   = sb.ToString().ToUtf8Bytes();
        _pagination = new Components.Pagination(
            current:    currentPage,
            total:      totalPages,
            urlPattern: "/blog?page={0}");
    }

    protected override void RenderPostList(HtmxRenderContext ctx)  => ctx.Writer.WriteUtf8(_postList);
    protected override void RenderPagination(HtmxRenderContext ctx) => _pagination.Render(ctx.Next());
}

GET handler

[Handler]
[MapGet("/blog")]
public static partial class GetBlogHandler
{
    public record Query([property: FromQuery] int Page = 1);

    private static async Task<IResult> HandleAsync(
        Query q, HttpContext ctx, BlogService blog, CancellationToken ct)
    {
        const int pageSize = 10;
        var (posts, total) = await blog.GetPageAsync(q.Page, pageSize, ct);
        int totalPages = (int)Math.Ceiling(total / (double)pageSize);

        return await ctx.WriteHtmxPage(
            new BlogPage(posts, q.Page, totalPages), title: "Blog");
    }
}