A single <div lvt-scroll-sentinel> at the end of the list is watched by the
client's IntersectionObserver. When it scrolls into view the client dispatches the
load_more action on its own — no client JS to wire up. The handler is nearly
identical to Click To Load — it just
pages a larger dataset; only the trigger really differs. When the last page arrives,
HasMore goes false and the sentinel is removed so it stops firing.
Infinite Scroll
A single <div lvt-scroll-sentinel> at the end of the list is watched by the client's IntersectionObserver. When it enters the viewport, the client dispatches load_more automatically — no client JS to wire up. Dataset has 100 items; scroll to watch pages append.
ID
Name
Email
1
Row 1
row1@example.com
2
Row 2
row2@example.com
3
Row 3
row3@example.com
4
Row 4
row4@example.com
5
Row 5
row5@example.com
6
Row 6
row6@example.com
7
Row 7
row7@example.com
8
Row 8
row8@example.com
9
Row 9
row9@example.com
10
Row 10
row10@example.com
Loading more…
Template
The sentinel <div lvt-scroll-sentinel> doubles as the loading indicator; once
HasMore is false it is replaced by an "End of list" note.
{{define "content"}}
<article>
<h3>Infinite Scroll</h3>
<p><small>A single <code><div lvt-scroll-sentinel></code> at the end of the list is watched by the client's IntersectionObserver. When it enters the viewport, the client dispatches <code>load_more</code> automatically — no client JS to wire up. Dataset has 100 items; scroll to watch pages append.</small></p>
<table>
<thead><tr><th>ID</th><th>Name</th><th>Email</th></tr></thead>
<tbody>
{{range .Items}}
<tr data-key="{{.ID}}">
<td>{{.ID}}</td>
<td>{{.Name}}</td>
<td>{{.Email}}</td>
</tr>
{{end}}
</tbody>
</table>
{{if .HasMore}}
<div lvt-scroll-sentinel><small aria-busy="true">Loading more…</small></div>
{{else}}
<p><small>End of list.</small></p>
{{end}}
</article>
{{end}}