Components and hooks for navigating through paged data with cursor-based and offset-based pagination strategies.
A pagination component for use with cursor-based pagination. Cursor-based pagination loads data in chunks by using a cursor from the last item on the current page to know where to start the next set. It doesn't let you jump to a specific page or know how many total pages there are, but it's more efficient for large or real-time data sets.
1import { CursorPagination } from "@ngrok/mantle/pagination";2 3<CursorPagination.Root defaultPageSize={100}>4 <CursorPagination.PageSizeSelect />5 <CursorPagination.Buttons hasNextPage hasPreviousPage />6</CursorPagination.Root>7 8<CursorPagination.Root defaultPageSize={100}>9 <CursorPagination.PageSizeValue />10 <CursorPagination.Buttons hasNextPage hasPreviousPage />11</CursorPagination.Root>12 13<CursorPagination.Root defaultPageSize={20}>14 <CursorPagination.Buttons hasNextPage hasPreviousPage={false} />15</CursorPagination.Root>CursorPagination pairs with a DataTable — drive its Buttons and PageSizeSelect from a TanStack Table instance.
Client-paginated — the table holds every row and getPaginationRowModel() slices them. Read getCanPreviousPage() / getCanNextPage() for the buttons and call setPageSize() / setPageIndex(0) when the size changes:
1import {2 DataTable,3 getCoreRowModel,4 getPaginationRowModel,5 useReactTable,6} from "@ngrok/mantle/data-table";7import { CursorPagination } from "@ngrok/mantle/pagination";8 9// defaultPageSize seeds an UNCONTROLLED <Select>, so keep it stable — a module10// const (or the table's INITIAL page size), never the live page size.11const DEFAULT_PAGE_SIZE = 10;12 13function PaginatedTable({ data }: { data: Payment[] }) {14 const table = useReactTable({15 data,16 columns,17 getCoreRowModel: getCoreRowModel(),18 getPaginationRowModel: getPaginationRowModel(),19 initialState: { pagination: { pageSize: DEFAULT_PAGE_SIZE } },20 });21 22 return (23 <>24 <DataTable.Root table={table}>{/* … Head + Body … */}</DataTable.Root>25 <CursorPagination.Root className="flex justify-end" defaultPageSize={DEFAULT_PAGE_SIZE}>26 <CursorPagination.PageSizeSelect27 onChangePageSize={(size) => {28 table.setPageSize(size);29 table.setPageIndex(0); // reset to the first page when the size changes30 }}31 />32 <CursorPagination.Buttons33 hasPreviousPage={table.getCanPreviousPage()}34 hasNextPage={table.getCanNextPage()}35 onPreviousPage={() => table.previousPage()}36 onNextPage={() => table.nextPage()}37 />38 </CursorPagination.Root>39 </>40 );41}Server / cursor data — the server returns one page at a time, so there is no getPaginationRowModel and no total count. Wire Buttons to your fetch's has-next / has-prev and load callbacks, and show the fixed, server-controlled page size with the read-only PageSizeValue instead of PageSizeSelect:
1import { CursorPagination } from "@ngrok/mantle/pagination";2 3<CursorPagination.Root className="flex justify-end" defaultPageSize={pageSize}>4 <CursorPagination.PageSizeValue />5 <CursorPagination.Buttons6 hasPreviousPage={Boolean(previousCursor)}7 hasNextPage={Boolean(nextCursor)}8 onPreviousPage={() => loadPage(previousCursor)}9 onNextPage={() => loadPage(nextCursor)}10 />11</CursorPagination.Root>;See the DataTable pagination recipe for the full table markup.
Compose the parts of a CursorPagination together to build your own:
CursorPagination.Root├── CursorPagination.PageSizeSelect├── CursorPagination.PageSizeValue└── CursorPagination.ButtonsThe root container for cursor-based pagination. Manages the page size state.
All props from the HTML div element, plus:
A pair of previous/next buttons for navigating between pages.
A select input for changing the number of items per page.
Displays the current page size as a read-only value (e.g. "100 per page").
A headless hook for managing offset-based pagination state. Offset-based pagination lets you jump to a specific page and know the total number of pages.
1import { useOffsetPagination } from "@ngrok/mantle/pagination";2 3const pagination = useOffsetPagination({4 listSize: 150,5 pageSize: 10,6});7 8// pagination.currentPage — current page number (1-indexed)9// pagination.totalPages — total number of pages10// pagination.hasNextPage — whether there is a next page11// pagination.hasPreviousPage — whether there is a previous page12// pagination.nextPage() — go to next page13// pagination.previousPage() — go to previous page14// pagination.goToPage(n) — go to a specific page15// pagination.goToFirstPage() — go to the first page16// pagination.goToLastPage() — go to the last page17// pagination.offset — the offset of the current page18// pagination.pageSize — the current page size19// pagination.setPageSize(n) — set the page size (resets to page 1)A utility function to get a paginated slice of a list based on the current offset pagination state.
1import { getOffsetPaginatedSlice, useOffsetPagination } from "@ngrok/mantle/pagination";2 3const data = ["a", "b", "c", "d", "e", "f"];4const pagination = useOffsetPagination({ listSize: data.length, pageSize: 2 });5const currentPageData = getOffsetPaginatedSlice(data, pagination);6// Returns: ['a', 'b'] for page 1, ['c', 'd'] for page 2, etc.