LoadMore
Adds the next page to the bottom of a list, by scrolling or with a button.
Preview
tsx
import { derived, state } from '@kerf-js/core'import { LoadMore, Row, Shape, Stack, Text} from '@kerf-js/veneer'export function OnPress() { const shown = state(3) const loading = state(false) const load = () => { loading.value = true setTimeout(() => { shown.value += 3 loading.value = false }, 800) } const done = derived( () => shown.value >= PROJECTS.length, ) return ( <Stack gap="related"> {PROJECTS.slice(0, shown.value).map((p) => ( <ProjectRow key={p.id} name={p.name} owner={p.owner} /> ))} {loading.value && ( <Shape kind="text" lines={2} label="Loading projects" /> )} <LoadMore auto={false} loading={loading.value} done={done.value} message={ done.value ? 'That is every project.' : `${shown.value} of ${PROJECTS.length}` } onLoad={load} /> </Stack> )}type Status = | 'Live' | 'Building' | 'Failed' | 'Paused'const PROJECTS: { id: string name: string owner: string region: string status: Status deploys: number uptime: number latency: number cost: number updated: string}[] = [ { id: 'storefront', name: 'storefront', owner: 'Priya Raman', region: 'eu-west', status: 'Live', deploys: 412, uptime: 99.98, latency: 84, cost: 1240, updated: '2026-10-31', }, { id: 'checkout', name: 'checkout', owner: 'Tomás Oliveira', region: 'eu-west', status: 'Live', deploys: 288, uptime: 99.95, latency: 112, cost: 980, updated: '2026-10-31', }, { id: 'search-api', name: 'search-api', owner: 'Lena Fischer', region: 'us-east', status: 'Building', deploys: 196, uptime: 99.91, latency: 146, cost: 1610, updated: '2026-10-30', }, { id: 'billing', name: 'billing', owner: 'Mara Lindqvist', region: 'us-east', status: 'Live', deploys: 154, uptime: 99.99, latency: 73, cost: 720, updated: '2026-10-29', }, { id: 'mobile-bff', name: 'mobile-bff', owner: 'Sam Okafor', region: 'ap-south', status: 'Failed', deploys: 233, uptime: 99.42, latency: 208, cost: 860, updated: '2026-10-29', }, { id: 'notifier', name: 'notifier', owner: 'Jun Tanaka', region: 'eu-north', status: 'Live', deploys: 97, uptime: 99.97, latency: 58, cost: 310, updated: '2026-10-27', }, { id: 'reports', name: 'reports', owner: 'Noor Haddad', region: 'us-west', status: 'Paused', deploys: 41, uptime: 99.8, latency: 321, cost: 190, updated: '2026-10-22', }, { id: 'edge-cache', name: 'edge-cache', owner: 'Jun Tanaka', region: 'global', status: 'Live', deploys: 318, uptime: 100, latency: 21, cost: 1430, updated: '2026-10-31', }, { id: 'auth', name: 'auth', owner: 'Mara Lindqvist', region: 'eu-west', status: 'Live', deploys: 176, uptime: 99.99, latency: 66, cost: 540, updated: '2026-10-28', }, { id: 'ingest', name: 'ingest', owner: 'Lena Fischer', region: 'us-east', status: 'Live', deploys: 264, uptime: 99.93, latency: 131, cost: 2120, updated: '2026-10-30', },]function ProjectRow({ name, owner,}: { name: string owner: string}) { return ( <Row justify="between" gap="group" > <Text>{name}</Text> <Text subtle>{owner}</Text> </Row> )}Installation
npm install @kerf-js/veneerTry it
Change the controls: the preview and its code update together. The address keeps what you set, so you can link to it.
Live preview
Controls
Codeupdates as you change the controls
import { LoadMore } from '@kerf-js/veneer'<LoadMore auto={false} message="That is every project." loading={loading.value} done={done()} onLoad={load} />Usage
tsx
import { LoadMore } from '@kerf-js/veneer'While scrolling
Loads as the end comes near. Pass root when the list scrolls in its own box.
tsx
import { derived, state } from '@kerf-js/core'import { LoadMore, Row, Stack, Text} from '@kerf-js/veneer'export function Scrolling() { const shown = state(6) const loading = state(false) let box: HTMLElement | undefined const load = () => { loading.value = true setTimeout(() => { shown.value += 4 loading.value = false }, 700) } const done = derived( () => shown.value >= PROJECTS.length, ) return ( <Stack gap="related" ref={(n: HTMLElement) => (box = n)} style="block-size:220px;overflow-y:auto" > {PROJECTS.slice(0, shown.value).map((p) => ( <ProjectRow key={p.id} name={p.name} owner={p.owner} /> ))} <LoadMore root={() => box ?? null} loading={loading.value} done={done.value} message={ done.value ? 'That is every project.' : '' } onLoad={load} /> </Stack> )}type Status = | 'Live' | 'Building' | 'Failed' | 'Paused'const PROJECTS: { id: string name: string owner: string region: string status: Status deploys: number uptime: number latency: number cost: number updated: string}[] = [ { id: 'storefront', name: 'storefront', owner: 'Priya Raman', region: 'eu-west', status: 'Live', deploys: 412, uptime: 99.98, latency: 84, cost: 1240, updated: '2026-10-31', }, { id: 'checkout', name: 'checkout', owner: 'Tomás Oliveira', region: 'eu-west', status: 'Live', deploys: 288, uptime: 99.95, latency: 112, cost: 980, updated: '2026-10-31', }, { id: 'search-api', name: 'search-api', owner: 'Lena Fischer', region: 'us-east', status: 'Building', deploys: 196, uptime: 99.91, latency: 146, cost: 1610, updated: '2026-10-30', }, { id: 'billing', name: 'billing', owner: 'Mara Lindqvist', region: 'us-east', status: 'Live', deploys: 154, uptime: 99.99, latency: 73, cost: 720, updated: '2026-10-29', }, { id: 'mobile-bff', name: 'mobile-bff', owner: 'Sam Okafor', region: 'ap-south', status: 'Failed', deploys: 233, uptime: 99.42, latency: 208, cost: 860, updated: '2026-10-29', }, { id: 'notifier', name: 'notifier', owner: 'Jun Tanaka', region: 'eu-north', status: 'Live', deploys: 97, uptime: 99.97, latency: 58, cost: 310, updated: '2026-10-27', }, { id: 'reports', name: 'reports', owner: 'Noor Haddad', region: 'us-west', status: 'Paused', deploys: 41, uptime: 99.8, latency: 321, cost: 190, updated: '2026-10-22', }, { id: 'edge-cache', name: 'edge-cache', owner: 'Jun Tanaka', region: 'global', status: 'Live', deploys: 318, uptime: 100, latency: 21, cost: 1430, updated: '2026-10-31', }, { id: 'auth', name: 'auth', owner: 'Mara Lindqvist', region: 'eu-west', status: 'Live', deploys: 176, uptime: 99.99, latency: 66, cost: 540, updated: '2026-10-28', }, { id: 'ingest', name: 'ingest', owner: 'Lena Fischer', region: 'us-east', status: 'Live', deploys: 264, uptime: 99.93, latency: 131, cost: 2120, updated: '2026-10-30', },]function ProjectRow({ name, owner,}: { name: string owner: string}) { return ( <Row justify="between" gap="group" > <Text>{name}</Text> <Text subtle>{owner}</Text> </Row> )}Loading
The button stays, disabled, and the words are announced.
tsx
import { LoadMore } from '@kerf-js/veneer'export function Loading() { return ( <LoadMore auto={false} loading onLoad={() => {}} /> )}Nothing left
done swaps the button for your message.
tsx
import { LoadMore } from '@kerf-js/veneer'export function Done() { return ( <LoadMore auto={false} done message="That is every deploy from October." onLoad={() => {}} /> )}When to use
Use it for
- A feed people read from the top down
Reach for something else
- PaginationBar, when people need to find their way back to a place
Accessibility
- The button is always there, even with
auto, so nobody depends on scrolling. - The message is announced as it changes.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
loading | boolean | false | A page is on its way. Stops further calls. |
done | boolean | false | There is nothing left. Removes the button. |
message | unknown | — | Words beside the button, or in its place once done. |
auto | boolean | true | Load when the end scrolls into view. |
root | (() => HTMLElement | null) | — | The box that scrolls, if it isn't the window. |
| Event | Type | Default | Description |
|---|---|---|---|
onLoad | () => void | — | Called when more is wanted, by a press or by reaching the end. |