Files
Htmx/docs/Components/Pagination.md
T
2026-05-05 23:55:26 +05:00

4.5 KiB
Raw Blame History

Pagination

A row of numbered page links — Previous, 1, 2, 3…, Next. Use it at the bottom of a list or table when there are too many items to show all at once. You give it the current page number, the total number of pages, and a URL pattern; it builds all the links automatically.


Quick example

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

This renders a navigation row with links to pages 110 (with ellipsis for interior pages) and the Previous/Next arrows.


All the options

public Pagination(
    int    current,
    int    total,
    string urlPattern)
Parameter What it does
current The currently active page. 1-based (the first page is 1).
total The total number of pages.
urlPattern A URL with {0} where the page number goes. E.g. "/items?page={0}".

The visible page window is at most 7 buttons. For large page counts, interior pages collapse into ellipsis () while the first page, last page, and pages close to current stay visible.


Real-world examples

Basic list with pagination

<!-- Templates/BlogPage.htmx -->
<div class="space-y-6 mb-10">$$Posts$$</div>
$$Pager$$
// Templates/BlogPage.htmx.cs
_pager = new Pagination(
    current:    page,
    total:      totalPages,
    urlPattern: "/blog?page={0}");

Preserving filters and sort order across pages

Build the URL pattern to include any query parameters that should survive page navigation:

var urlPattern = $"/users?role={role}&sort={sort}&page={{0}}";
new Pagination(current: page, total: totalPages, urlPattern: urlPattern)

Note the double braces {{0}} to produce a literal {0} after string interpolation.

HTMX-powered pagination (no full page reload)

Wrap the pagination (and the content it controls) in a hx-boost container:

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

How it works

All links are plain <a href="..."> elements — no JavaScript required. The URL for each page is built by calling string.Format(urlPattern, pageNumber). When current == 1, the Previous link is styled as disabled (pointer-events removed, opacity reduced); same for Next when current == total.


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");
    }
}