Skip to content

Live demo

Every widget on this page is a real engine talking to a real, free, key-free public API. The docs site is a Vue app, so the demos run on @scrollstackjs/vue — the React and Svelte adapters expose the same snapshot and the same controls.

No code here on purpose: play with the behavior first, then follow the link under each demo to the part of the tutorial that explains it, in all three frameworks.

Offline or rate-limited, you'll see the error states instead — which is itself one of the demos.

Auto-loading feed

The default arrangement: a sentinel rendered while hasNextPage is true, and the observer loading the next page when it scrolls into view. The page param here is a URL string, not a number — getNextPageParam returns whatever the API hands back. The panel on the right is the live snapshot; watch fetchStatus flip to fetching and isFetchingNextPage light up as you scroll.

rickandmortyapi.com — cursor pagination

How it works → Tutorial § 2, Your first feed — the three required options, in Vue, React, and Svelte.

Errors, retry, and why your list survives

Flip Break the next fetch and scroll — the next request goes to ?page=9999, which really does 404. The engine retries twice on a short backoff (failureCount climbs while error is still null) and only then gives up. Notice what doesn't happen: the rows already loaded stay exactly where they were, and status remains success.

Errors & retry

Uncheck the toggle, then press Retryretry() clears failureCount and error and resumes from the page that failed. Nothing already loaded is refetched.

How it works → Tutorial § 3, The five states you actually render for the loadMoreFailed check, and § 5, Retry settings for retry and retryDelay.

Manual controls

Both ways into the same engine, over the PokéAPI's offset pagination. autoLoad is on, so scrolling to the sentinel loads the next ten — and the button calls loadNextPage() directly for the same result.

They can't race each other: loadNextPage() no-ops while a fetch is in flight or once hasNextPage is false, so neither path needs a guard and the disabled binding is purely cosmetic. Set autoLoad: false to make the button the only way in.

pokeapi.co — offset, autoLoad: true + a button
scroll here, or use the button below

How it works → Tutorial § 6, Driving it yourself for every control, or § 5, Observer settings to toggle autoLoad live.

Horizontal rail

Same engine, sideways. root is the rail element and rootMargin puts the trigger 240px before its right edge, so a page is already in flight before you reach the end.

Horizontal — root is the rail

Because options are read once, the component that calls the composable has to mount inside the container — hence the parent/child split.

How it works → Horizontal & scoped scrolling for the full pattern, or Tutorial § 5, Observer settings to change root, rootMargin, and threshold live.

Pagination is one function

Three engines, three different real APIs, one interface. Only getNextPageParam differs — press Next page on each and watch the params accumulate.

Cursor rickandmortyapi.com

getNextPageParam: (last) => last.next
pages
0
hasNextPage
true
  1. no pages fetched yet

Offset / limit pokeapi.co

getNextPageParam: (page, _all, param) =>
  page.length === LIMIT ? param + LIMIT : null
pages
0
hasNextPage
true
  1. no pages fetched yet

Page number jsonplaceholder.typicode.com

getNextPageParam: (page, _all, param) =>
  page.length === LIMIT ? param + 1 : null
pages
0
hasNextPage
true
  1. no pages fetched yet

The cursor column collects URLs, the offset column counts 0, 10, 20…, and the page column counts 1, 2, 3…. Note that 0 is a perfectly valid param — the engine checks == null, never truthiness, so an offset of zero is a real page rather than the end of the list.

How it works → Tutorial § 4, Pick your pagination, or Pagination for the full guide.

Cancellation and stale results

This one hits the API for real, then holds the result for 2.5 seconds so you can catch it mid-flight. Start a fetch, then hit Reset mid-flight: the signal aborts, the late response is discarded by the generation counter, and failureCount stays at 0 — an abort is a cancellation, not a failure.

Cancellation & stale results
0fetches started
0signals aborted
0responses kept
0pages in state

Forwarding signal is the whole contract. Even if you ignore it, the generation guard still makes the stale result inert — the signal just saves the bandwidth.

How it works → Tutorial § 2, Your first feed covers the signal contract.

Events, via a plugin

The log below isn't wired with engine.on in the component — it's a plugin, a function that receives the engine, subscribes, and returns a cleanup that runs on destroy(). Plugins are registered at creation, so they never miss the first loadStart.

jsonplaceholder — events via a plugin
  1. No events yet — load a page, then reset.

How it works → Tutorial § 7, Watching what happens — callbacks, events, and plugins, and when to reach for each.

Devtools, on a real engine

The panel below is the actual @scrollstackjs/devtools build reading the engine next to it — not a screenshot, and not a reimplementation for the docs.

Press Open devtools, then load a page and watch the timeline record loadStartsuccess with the duration of each fetch. Tick Break the next fetch to see an error row and the load-more indicator, which distinguishes "first load failed (no data)" from "load-more failed (data intact)". The Pages tab shows each loaded page against the pageParam that fetched it.

rickandmortyapi — inspected live

Open the panel, then load a page — the timeline records every step.

The panel is position: fixed, exactly as in your own app, so it floats over this page rather than sitting in the box — drag its header to move it, or press Ctrl+Shift+0 to toggle it. It closes when you navigate away.

How it works → @scrollstackjs/devtools for the full API, or Events & plugins for the one-line devtoolsPlugin() form used in real apps.

What isn't here

Nothing on this page is faked. Virtualization has its own package — see the virtual list example. Persistence, pull-to-refresh and backwards (getPreviousPageParam) paging are not built yet.

Ready to write some? The examples have the code for everything on this page, or start at the tutorial.