Paginated list
Prev/next plus numbered pages, rendered from a single GET. Htmx swaps the table in place — no full navigation.
#When to use
Classic catalog/table paging. For "load more"-style experiences, see Infinite scroll.
#Markup
<div id="products_table">
<partial name="_ProductsTable" model="Model" />
<nav>
@for (int p = 1; p <= totalPages; p++)
{
<a hx-get="/products?page=@p"
hx-target="#products_table"
hx-swap="outerHTML"
hx-push-url="true"
href="/products?page=@p"
class="@(p == currentPage ? "active" : "")">@p</a>
}
</nav>
</div>
hx-push-url="true"keeps the address bar honest, so refresh / share / back-button all land on the right page.- The
hrefis preserved as a real URL — users without JS still get a working page-link.
#Handler
public async Task<IActionResult> OnGetAsync(int page = 1, int pageSize = 20)
{
var results = await _products.ListPageAsync(page, pageSize);
return this.PageOrPartial("_ProductsTable", results);
}
PageOrPartial returns the full page on a normal navigation and the _ProductsTable partial when htmx requests it (detected via the HX-Request header).
#Preserve other query state
When pagination coexists with sort / search, every link needs to carry the full state:
<a hx-get="/products?page=@p&sort=@sort&dir=@dir&q=@q"
hx-target="#products_table"
hx-swap="outerHTML"
hx-push-url="true"
href="/products?page=@p&sort=@sort&dir=@dir&q=@q">@p</a>
The Pagination component in ndui add pagination handles this automatically when you bind it to the current IDictionary of query params.