caliper uidocs

Load more

Free

The end of a long list: the next page loads as it comes into view, failures offer a retry, a button does the same for keyboards, and the end says so.

Installs components/ui/load-more.tsxDepends on @radix-ui/react-slot, lucide-reactMotion none in component source
pnpm dlx shadcn@latest add https://caliperui.site/r/load-more.json
  • Card payment 001−€12.00
  • Card payment 002−€49.00
  • Card payment 003−€86.00
  • Card payment 004−€123.00
  • Card payment 005−€160.00
  • Card payment 006−€17.00
  • Card payment 007−€54.00
  • Card payment 008−€91.00
  • Card payment 009−€128.00
  • Card payment 010−€165.00
  • Card payment 011−€22.00
  • Card payment 012−€59.00

When to use #

Use it for

  • Feeds, search results and galleries.
  • Lists where pages are an implementation detail.

Skip it when

  • Tables people jump around in; use pagination.

Installation #

terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/load-more.json

Adds its 3 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.

Usage #

This is the code behind the preview above.

example.tsx
import { useState } from "react"
import { Checkbox } from "@/components/ui/checkbox"
import { LoadMore } from "@/components/ui/load-more"

const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));

export function LoadMoreDemo() {
  const [rows, setRows] = useState(12);
  const [failNext, setFailNext] = useState(false);
  return (
    <div className="grid w-full max-w-md gap-3">
      <label className="flex items-center gap-2 text-sm"><Checkbox checked={failNext} onChange={(event) => setFailNext(event.target.checked)} />Fail the next page</label>
      <div className="h-[340px] overflow-y-auto rounded-[14px] shadow-[inset_0_0_0_1px_var(--border)]" tabIndex={0} role="region" aria-label="Transactions">
        <ul className="grid">
          {Array.from({ length: rows }, (_, index) => (
            <li key={index} className="flex justify-between border-b border-border px-4 py-3 text-sm">
              <span>Card payment {String(index + 1).padStart(3, "0")}</span>
              <span className="font-mono text-muted-foreground">−€{(12 + ((index * 37) % 180)).toFixed(2)}</span>
            </li>
          ))}
        </ul>
        <LoadMore
          hasMore={rows < 48}
          onLoad={async () => {
            await wait(900);
            if (failNext) {
              setFailNext(false);
              throw new Error("Network");
            }
            setRows((n) => n + 12);
          }}
          endLabel="That is every transaction this month"
        />
      </div>
    </div>
  );
}

API reference #

LoadMore

component
PropTypeDefaultDescription
onLoad *() => Promise<void>—Loads the next page; a thrown error shows the retry.
hasMore *boolean—
autobooleantrueLoad as soon as the end scrolls into view.
labelstring"Load more"
endLabelstring"You have reached the end"
classNamestring—

Accessibility #

It exposes status and alert semantics, with aria-hidden kept in step with state. Changes that matter are announced through a live region.

No custom key handling; Tab and the native activation keys do the work.

Motion #

It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.