Headless infinite scroll: sentinel-driven paging, load-more fallback, live status, and APG feed.
|
|
| Package |
@vizejs/ui/infinite-scroll |
| Maturity |
stable |
| Own the source |
vize lib pull infinite-scroll |
| Requires |
context, id, measure |
| Aliases |
infinite scroll, load more, feed, endless scroll, pagination sentinel |
| Covers |
WAI-ARIA Feed, IntersectionObserver, VueUse useInfiniteScroll, React Aria useLoadMore |
Usage
import { InfiniteScrollItem, InfiniteScrollLoadMore, InfiniteScroll, InfiniteScrollRoot, InfiniteScrollSentinel, InfiniteScrollStatus } from "@vizejs/ui/infinite-scroll";
Or copy the source into your project with vize lib pull infinite-scroll (see Source Distribution).
API
Source: src/families/data/infinite-scroll/infinite-scroll-item.vue
Props
| Prop |
Type |
Default |
Description |
index (required) |
number |
required |
Zero-based item position, announced one-based through aria-posinset. |
ariaLabelledby |
string |
undefined |
Ids of the article title, per the feed pattern. |
ariaDescribedby |
string |
undefined |
Ids of the article's primary content. |
Slots
| Slot |
Slot props |
Description |
default |
InfiniteScrollItemSlotState |
Article content. Receives the announced position and set size. |
Exposed
| Member |
Type |
Description |
position |
number |
One-based position announced through aria-posinset. |
setSize |
number |
Announced set size; -1 while the total is unknown. |
element |
HTMLElement | null |
Rendered article element. |
Source: src/families/data/infinite-scroll/infinite-scroll-load-more.vue
Props
| Prop |
Type |
Default |
Description |
hideWhenComplete |
boolean |
true |
Apply the native hidden attribute once every item is loaded. |
Events
| Event |
Payload |
Description |
click |
[nativeEvent: MouseEvent] |
Fired before the request. Call preventDefault() to keep state unchanged. |
Slots
| Slot |
Slot props |
Description |
default |
InfiniteScrollSlotState |
Button label. Receives the loading state, e.g. to render "Retry" after an error. |
Exposed
| Member |
Type |
Description |
state |
InfiniteScrollState |
Current loading state. |
busy |
boolean |
Whether a request is in flight. |
hasMore |
boolean |
Whether more items can be requested. |
error |
unknown |
Rejection reason of the last failed loader call, or undefined. |
element |
HTMLButtonElement | null |
Rendered native button. |
Source: src/families/data/infinite-scroll/infinite-scroll-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned root id. null and undefined select a deterministic fallback. |
hasMore |
boolean |
true |
Whether more items can be requested. false settles the state to complete. |
loader |
InfiniteScrollLoader |
undefined |
Page loader called for every accepted request. A returned promise keeps the state loading until it settles; a rejection moves to error. |
loading |
boolean |
undefined |
Controlled in-flight flag for consumers that load through the loadMore emit. undefined derives the flag from loader. |
disabled |
boolean |
false |
Suppress every request while keeping rendered items intact. |
scrollRoot |
InfiniteScrollObserverRoot |
"viewport" |
Intersection root: the viewport, or this root element as the scroll container. |
rootMargin |
string |
"256px" |
Margin around the intersection root, so pages load before the sentinel is visible. |
feed |
boolean |
false |
Render the WAI-ARIA feed pattern: role="feed", aria-busy, and PageUp/PageDown focus movement between InfiniteScrollItem articles. |
total |
number |
undefined |
Total item count announced as aria-setsize. undefined announces -1 (unknown). |
ariaLabel |
string |
undefined |
Accessible feed name when no visible label or aria-labelledby supplies one. |
ariaLabelledby |
string |
undefined |
Space-separated ids that label the feed. |
Events
| Event |
Payload |
Description |
loadMore |
[trigger: InfiniteScrollTrigger] |
Fired for every accepted request before loader runs. |
error |
[reason: unknown, trigger: InfiniteScrollTrigger] |
Fired after a loader promise rejects. |
stateChange |
[state: InfiniteScrollState, previous: InfiniteScrollState] |
Fired after every distinct loading-state transition. |
Slots
| Slot |
Slot props |
Description |
default |
InfiniteScrollSlotState |
Items, sentinel, load-more control, and status. Receives the loading state. |
Exposed
| Member |
Type |
Description |
busy |
— |
|
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
error |
— |
|
hasMore |
— |
|
loadMore |
— |
|
refresh |
— |
|
retry |
— |
|
state |
— |
|
Source: src/families/data/infinite-scroll/infinite-scroll-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned root id. null and undefined select a deterministic fallback. |
hasMore |
boolean |
true |
Whether more items can be requested. false settles the state to complete. |
loader |
InfiniteScrollLoader |
undefined |
Page loader called for every accepted request. A returned promise keeps the state loading until it settles; a rejection moves to error. |
loading |
boolean |
undefined |
Controlled in-flight flag for consumers that load through the loadMore emit. undefined derives the flag from loader. |
disabled |
boolean |
false |
Suppress every request while keeping rendered items intact. |
scrollRoot |
InfiniteScrollObserverRoot |
"viewport" |
Intersection root: the viewport, or this root element as the scroll container. |
rootMargin |
string |
"256px" |
Margin around the intersection root, so pages load before the sentinel is visible. |
feed |
boolean |
false |
Render the WAI-ARIA feed pattern: role="feed", aria-busy, and PageUp/PageDown focus movement between InfiniteScrollItem articles. |
total |
number |
undefined |
Total item count announced as aria-setsize. undefined announces -1 (unknown). |
ariaLabel |
string |
undefined |
Accessible feed name when no visible label or aria-labelledby supplies one. |
ariaLabelledby |
string |
undefined |
Space-separated ids that label the feed. |
Events
| Event |
Payload |
Description |
loadMore |
[trigger: InfiniteScrollTrigger] |
Fired for every accepted request before loader runs. |
error |
[reason: unknown, trigger: InfiniteScrollTrigger] |
Fired after a loader promise rejects. |
stateChange |
[state: InfiniteScrollState, previous: InfiniteScrollState] |
Fired after every distinct loading-state transition. |
Slots
| Slot |
Slot props |
Description |
default |
InfiniteScrollSlotState |
Items, sentinel, load-more control, and status. Receives the loading state. |
Exposed
| Member |
Type |
Description |
state |
InfiniteScrollState |
Current loading state. |
busy |
boolean |
Whether a request is in flight. |
hasMore |
boolean |
Whether more items can be requested. |
error |
unknown |
Rejection reason of the last failed loader call, or undefined. |
element |
HTMLDivElement | null |
Rendered root element. |
loadMore |
() => boolean |
Request the next page like the sentinel would. Reports whether a request started. |
retry |
() => boolean |
Clear an error and request again. Reports whether a request started. |
refresh |
() => void |
Re-check sentinel visibility, e.g. after a layout change without a state change. |
Source: src/families/data/infinite-scroll/infinite-scroll-sentinel.vue
Slots
| Slot |
Slot props |
Description |
default |
InfiniteScrollSlotState |
Optional sentinel content such as a spinner. Receives the loading state. |
Exposed
| Member |
Type |
Description |
element |
HTMLDivElement | null |
Rendered sentinel element. |
intersecting |
boolean |
Whether the sentinel currently intersects its root. |
Source: src/families/data/infinite-scroll/infinite-scroll-status.vue
Slots
| Slot |
Slot props |
Description |
default |
InfiniteScrollSlotState |
Announcement text such as "Loading more results" or "All results loaded". The polite live region is always rendered so changes are announced. |
Exposed
| Member |
Type |
Description |
state |
InfiniteScrollState |
Current loading state. |
busy |
boolean |
Whether a request is in flight. |
hasMore |
boolean |
Whether more items can be requested. |
error |
unknown |
Rejection reason of the last failed loader call, or undefined. |
element |
HTMLDivElement | null |
Rendered live region. |
Behavior
Normative state x input -> outcome table for infinite-scroll-root.vue,
infinite-scroll-sentinel.vue, infinite-scroll-load-more.vue,
infinite-scroll-status.vue, and infinite-scroll-item.vue
(@vizejs/ui/infinite-scroll). Every row is proven by the named test.
The loading state is disabled > loading > complete > error > idle in priority
order. A request is accepted only while idle, or while error from the
load-more button or the imperative API, so a failing page never loops through
the sentinel. loader promises drive loading automatically; consumers that
fetch through the loadMore emit own the busy flag through the loading prop.
| ID |
State |
Input |
Outcome |
Evidence |
| IS1 |
idle |
render |
root carries a deterministic id, aria-busy="false", feed semantics when feed, observed hidden sentinel, wired button, live status |
renders sentinel, load-more, status, and feed semantics with deterministic wiring |
| IS2 |
idle |
sentinel intersects |
emits loadMore("sentinel") once, runs loader, and stays loading (aria-busy="true", button disabled) until the promise settles |
the sentinel requests once per intersection and the loader promise drives loading |
| IS3 |
loading -> idle |
loader resolves |
returns to idle and re-observes the sentinel, so a still-visible sentinel requests the next page |
the sentinel requests once per intersection and the loader promise drives loading |
| IS4 |
controlled loading |
prop changes / hasMore=false |
the parent owns the busy flag; complete hides and disables the button and ignores the sentinel |
controlled loading and hasMore settle state without a loader |
| IS5 |
loading |
loader rejects or throws |
moves to error, emits error(reason, trigger), exposes the reason, and ignores the sentinel until the button or API retries |
loader failures stop automatic loading until an explicit retry |
| IS6 |
idle |
button click |
emits click first; preventDefault() keeps state unchanged, otherwise requests with trigger button |
the load-more button requests pages and honors preventDefault |
| IS7 |
disabled |
intersection / click / API |
every request is refused |
disabled roots suppress every request |
| IS8 |
scrollRoot="self" |
mount / refresh() |
the sentinel observes against the root element with rootMargin; refresh() re-observes |
self scroll roots observe the sentinel against the root element |
| IS9 |
sentinel |
intersection batches |
exposes the latest intersection state and ignores empty batches |
the sentinel exposes its intersection state |
| IS10 |
feed |
PageDown / PageUp |
moves focus between articles; PageDown on the last article requests the next page; other keys and non-feed roots are untouched |
feed PageDown and PageUp move focus between articles and request at the end |
| IS11 |
exposed instance |
read / loadMore() / retry() |
exposes state, busy, hasMore, error, element; retry() requires error; unknown totals announce aria-setsize="-1" |
exposes typed state and imperative paging controls |
| IS12 |
missing provider |
setup |
compound parts fail closed with the shared context diagnostic |
compound parts require a matching root provider |
| IS13 |
SSR |
isolated requests |
markup is byte-identical, including generated ids, feed positions, and the live region |
renders byte-identical infinite scroll markup across isolated SSR requests |
| IS14 |
SSR / hydration |
hydrate |
server markup hydrates without warnings or node replacement |
hydrates infinite scroll markup without warnings or node replacement |
| IS15 |
types |
compile |
states, triggers, observer roots, slot state, and exposes are closed and read-only |
infinite-scroll.types.test-d.ts |
Without IntersectionObserver the sentinel stays inert and the load-more button
remains the paging control. The intersection root and margin are read when the
sentinel mounts. Control+Home/End feed shortcuts are optional in the APG pattern
and are not implemented.