ee8797c142
Co-authored-by: Copilot <copilot@github.com>
5.6 KiB
5.6 KiB
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
urlPatternusesstring.Format-style{0}— do not use{page}or other named placeholders. - Page numbers are 1-based throughout — the first page is page
1. - When
totalis 0 or negative the component renders nothing — guardtotal > 1in 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
urlPatternquery string. - For very small page counts (2–3 pages), all page buttons are shown with no ellipsis.
- For very small page counts (2–3 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");
}
}