Infinite scroll
A sentinel element at the bottom of the list triggers a fragment request when it enters the viewport — appending, not replacing.
#When to use
Activity feeds, comment threads, product grids where users expect to browse without clicking. For paginated tables, see Paginated list instead — it's far more accessible for deep navigation.
#Markup
<ul id="feed">
<partial name="_FeedItems" model="Model.Items" />
@if (Model.NextCursor is not null)
{
<li id="feed-sentinel"
hx-get="/feed?cursor=@Model.NextCursor"
hx-trigger="intersect once"
hx-target="#feed-sentinel"
hx-swap="outerHTML">
<noscript><a href="/feed?cursor=@Model.NextCursor">Load more</a></noscript>
</li>
}
</ul>
hx-trigger="intersect once"fires when the sentinel scrolls into view (theoncemodifier prevents re-firing).hx-swap="outerHTML"replaces the sentinel itself with the server's response — which contains the new items PLUS a fresh sentinel pointing at the next cursor.- The
fallback means users without JS can still click to load more.
#Handler
public async Task<IActionResult> OnGetAsync(string? cursor = null, CancellationToken ct = default)
{
var page = await _feed.PageAsync(cursor, pageSize: 20, ct);
return this.PageOrPartial("_FeedItems", page);
}
#The partial that returns BOTH new items and the next sentinel
@model FeedPage
@foreach (var item in Model.Items)
{
<li>@item.Title</li>
}
@if (Model.NextCursor is not null)
{
<li id="feed-sentinel"
hx-get="/feed?cursor=@Model.NextCursor"
hx-trigger="intersect once"
hx-target="#feed-sentinel"
hx-swap="outerHTML">
<noscript><a href="/feed?cursor=@Model.NextCursor">Load more</a></noscript>
</li>
}
Each fetch replaces the old sentinel with new items + a new sentinel. When the server runs out of pages it omits the sentinel and the chain stops naturally.
#Cursor vs offset
Prefer opaque cursors (e.g. the last item's id encoded base64) over numeric offsets. Offsets break when new items arrive between requests; cursors stay stable.
#Related
- Paginated list — the accessible alternative.
- Polling for progress — same
hx-triggerprimitive used as a tick.