Skip to content
kerfjsGitHub

LoadMore

Adds the next page to the bottom of a list, by scrolling or with a button.

SourcePackage @kerf-js/veneerTry it

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/veneer

Try 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

PropTypeDefaultDescription
loadingbooleanfalseA page is on its way. Stops further calls.
donebooleanfalseThere is nothing left. Removes the button.
messageunknown—Words beside the button, or in its place once done.
autobooleantrueLoad when the end scrolls into view.
root(() => HTMLElement | null)—The box that scrolls, if it isn't the window.
EventTypeDefaultDescription
onLoad() => void—Called when more is wanted, by a press or by reaching the end.

RelatedPagination barVirtual listFeed