Vize

Infinite Scroll

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

InfiniteScrollItem

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.

InfiniteScrollLoadMore

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.

InfiniteScroll

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 —

InfiniteScrollRoot

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.

InfiniteScrollSentinel

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.

InfiniteScrollStatus

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.