ComponentsTables and views

Pagination

A 36px line under the grid: how many records the filters hide, a way to clear them, and Next page when more rows wait.

Playground

Change the controls to try each prop. The preview shows the candidates line with both actions; the code lists only the props you changed.

States
import { Pagination } from '@/ui/pagination';<Pagination />

Under the grid

The line sits under the table, after the calculation footer, aligned to the cell padding. Both actions are quiet buttons.

CandidateCompanyUpdated
Yuki KimuraHalcyon Bank2h ago
Maya ChenNorthwind LogisticsYesterday
import { Pagination } from '@/ui/pagination';
import { Table, type TableColumn } from '@/ui/table';

type Candidate = { id: string; name: string; company: string; updated: string };

const candidates: Candidate[] = [
  { id: 'c1', name: 'Yuki Kimura', company: 'Halcyon Bank', updated: '2h ago' },
  { id: 'c2', name: 'Maya Chen', company: 'Northwind Logistics', updated: 'Yesterday' },
];

const columns: TableColumn<Candidate>[] = [
  { key: 'name', label: 'Candidate', cell: (row) => row.name },
  { key: 'company', label: 'Company', width: 200, cell: (row) => row.company },
  { key: 'updated', label: 'Updated', cell: (row) => row.updated },
];

export default function PaginationUnderTheGrid() {
  return (
    <div className="flex w-full flex-col bg-surface">
      <Table label="Candidates" columns={columns} rows={candidates} />
      <Pagination noun={{ one: 'candidate', many: 'candidates' }} hidden={8} onClear={() => {}} onNext={() => {}} />
    </div>
  );
}

What the line shows

Next page shows while more rows wait. The count shows while filters hide rows, in the singular for one. On the last page with nothing hidden there is no line at all; the footer count is enough.

More rows, nothing hidden

Last page, one row hidden

Many hidden

Last page, nothing hidden: no line at all

import { Pagination } from '@/ui/pagination';

const noun = { one: 'candidate', many: 'candidates' };

export default function PaginationWhatItShows() {
  return (
    <div className="flex w-full flex-col gap-200">
      <div className="flex flex-col gap-050">
        <p className="text-body-small leading-body-small text-text-subtle">More rows, nothing hidden</p>
        <Pagination noun={noun} onNext={() => {}} className="bg-surface" />
      </div>
      <div className="flex flex-col gap-050">
        <p className="text-body-small leading-body-small text-text-subtle">Last page, one row hidden</p>
        <Pagination noun={noun} hidden={1} onClear={() => {}} className="bg-surface" />
      </div>
      <div className="flex flex-col gap-050">
        <p className="text-body-small leading-body-small text-text-subtle">Many hidden</p>
        <Pagination noun={noun} hidden={12480} onClear={() => {}} onNext={() => {}} className="bg-surface" />
      </div>
      <div className="flex flex-col gap-050">
        <p className="text-body-small leading-body-small text-text-subtle">Last page, nothing hidden: no line at all</p>
        <Pagination noun={noun} />
      </div>
    </div>
  );
}

Long lists

A View with thousands of records loads 100 rows a page. While the next page loads, the rows dim, Next page ignores more presses, and its spinner shows only after 300ms. A page that fails to load says so in a toast that stays, with Try again; this example fails once on purpose.

CandidateNumber
Candidate 11
Candidate 22
Candidate 33
Candidate 44
Candidate 55
Candidate 66
Candidate 77
Candidate 88
Candidate 99
Candidate 1010
Candidate 1111
Candidate 1212
Candidate 1313
Candidate 1414
Candidate 1515
Candidate 1616
Candidate 1717
Candidate 1818
Candidate 1919
Candidate 2020
Candidate 2121
Candidate 2222
Candidate 2323
Candidate 2424
Candidate 2525
Candidate 2626
Candidate 2727
Candidate 2828
Candidate 2929
Candidate 3030
Candidate 3131
Candidate 3232
Candidate 3333
Candidate 3434
Candidate 3535
Candidate 3636
Candidate 3737
Candidate 3838
Candidate 3939
Candidate 4040
Candidate 4141
Candidate 4242
Candidate 4343
Candidate 4444
Candidate 4545
Candidate 4646
Candidate 4747
Candidate 4848
Candidate 4949
Candidate 5050
Candidate 5151
Candidate 5252
Candidate 5353
Candidate 5454
Candidate 5555
Candidate 5656
Candidate 5757
Candidate 5858
Candidate 5959
Candidate 6060
Candidate 6161
Candidate 6262
Candidate 6363
Candidate 6464
Candidate 6565
Candidate 6666
Candidate 6767
Candidate 6868
Candidate 6969
Candidate 7070
Candidate 7171
Candidate 7272
Candidate 7373
Candidate 7474
Candidate 7575
Candidate 7676
Candidate 7777
Candidate 7878
Candidate 7979
Candidate 8080
Candidate 8181
Candidate 8282
Candidate 8383
Candidate 8484
Candidate 8585
Candidate 8686
Candidate 8787
Candidate 8888
Candidate 8989
Candidate 9090
Candidate 9191
Candidate 9292
Candidate 9393
Candidate 9494
Candidate 9595
Candidate 9696
Candidate 9797
Candidate 9898
Candidate 9999
Candidate 100100
import { useState } from 'react';
import { Pagination } from '@/ui/pagination';
import { Table, type TableColumn } from '@/ui/table';
import { toast } from '@/ui/toast';

type Candidate = { id: string; name: string; number: number };

const total = 12480;
const pageSize = 100;
const pageOf = (page: number): Candidate[] =>
  Array.from({ length: pageSize }, (_, index) => {
    const number = page * pageSize + index + 1;
    return { id: `c${number}`, name: `Candidate ${number.toLocaleString('en-GB')}`, number };
  });

const columns: TableColumn<Candidate>[] = [
  { key: 'name', label: 'Candidate', cell: (row) => row.name },
  { key: 'number', label: 'Number', width: 120, align: 'end', cell: (row) => row.number.toLocaleString('en-GB') },
];

// The app loads 100 rows a page by cursor; the third page fails here, to show the error toast with Try again.
export default function PaginationLongLists() {
  const [page, setPage] = useState(0);
  const [loading, setLoading] = useState(false);
  const next = (attempt = 0) => {
    setLoading(true);
    setTimeout(() => {
      setLoading(false);
      if (page === 1 && attempt === 0) {
        toast.add({
          type: 'error',
          title: "Couldn't load the next page",
          description: 'Check your connection and try again.',
          actionProps: { children: 'Try again', onClick: () => next(1) },
        });
        return;
      }
      setPage((current) => current + 1);
    }, 900);
  };
  const last = (page + 1) * pageSize >= total;
  return (
    <div className="flex w-full flex-col bg-surface">
      <Table label={`Candidates, page ${page + 1}`} columns={columns} rows={pageOf(page)} loading={loading} className="max-h-60" />
      <Pagination noun={{ one: 'candidate', many: 'candidates' }} loading={loading} onNext={last ? undefined : () => next()} />
    </div>
  );
}

On this page