Paginated Collection
Auto-sizing grid/list container that uses ResizeObserver to fill available space and paginate automatically
Overview
PaginatedCollection measures its container with ResizeObserver and automatically calculates how many columns and rows fit based on minCardWidth and minCardHeight. Items are sliced into pages — no manual page-size calculation needed.
It is the rendering engine behind SearchableContainer. Use it directly when you don't need built-in search.
Installation
npx shadcn@latest add https://foodease-dev-registry.cap.reachcinema.io/r/v1/paginated-collection.jsonUsage
Basic Grid
import PaginatedCollection from "@/components/ui/paginated-collection"
<div className="flex-1 h-full">
<PaginatedCollection
items={products}
gridConfig={{ minCardWidth: 180, minCardHeight: 120, gap: 8 }}
renderItem={(product) => (
<div className="absolute inset-0 rounded-xl border p-2">
{product.name}
</div>
)}
/>
</div>The container must have a defined height (
h-full,h-[400px], etc.) — the component waits for a non-zeroResizeObservermeasurement before rendering.
List Layout
<PaginatedCollection
items={items}
layout="list"
gridConfig={{ minCardHeight: 56, gap: 4 }}
renderItem={(item) => (
<div className="absolute inset-0 flex items-center px-3 border-b">
{item.name}
</div>
)}
/>Custom Pagination
Override the default page buttons with your own UI:
<PaginatedCollection
items={items}
gridConfig={{ minCardWidth: 200, minCardHeight: 100, gap: 8 }}
renderItem={(item) => <ItemCard item={item} />}
renderPagination={({ currentPage, totalPages, onPageChange, hasNextPage, hasPreviousPage }) => (
<div className="flex justify-between items-center px-2 py-1">
<button disabled={!hasPreviousPage} onClick={() => onPageChange(currentPage - 1)}>
Previous
</button>
<span>{currentPage} / {totalPages}</span>
<button disabled={!hasNextPage} onClick={() => onPageChange(currentPage + 1)}>
Next
</button>
</div>
)}
/>Custom Empty State
<PaginatedCollection
items={[]}
renderItem={(item) => <ItemCard item={item} />}
renderEmpty={() => (
<div className="center h-full text-muted-foreground">
Nothing here yet
</div>
)}
/>Fixed Items Per Page
Override auto-calculation and fix the page size:
<PaginatedCollection
items={items}
itemsPerPage={12}
renderItem={(item) => <ItemCard item={item} />}
/>How Layout Is Calculated
cols = floor((containerWidth + gap) / (minCardWidth + gap))
rows = floor((containerHeight + gap) / (minCardHeight + gap))
itemsPerPage = cols × rowsThe container is measured on mount and on every resize. Page 1 is always shown when items or container size changes.
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | T[] | [] | Items to render |
layout | "grid" | "list" | "grid" | Layout mode. "list" uses a single column |
gridConfig | GridConfig | see below | Card size constraints |
renderItem | (item: T) => ReactNode | — | Render function per item |
renderEmpty | () => ReactNode | — | Render function when items is empty |
itemsPerPage | number | auto | Override auto page size |
renderPagination | (props: PaginationRenderProps) => ReactNode | — | Custom pagination UI |
onItemVisible | (item: T) => void | — | Called for each visible item on page |
className | string | — | Extra classes on the outer wrapper |
itemClassName | string | — | Extra classes on each item wrapper |
getItemKey | (item: T) => string | number | index | Key extractor |
GridConfig
| Field | Type | Default | Description |
|---|---|---|---|
minCardWidth | number | 320 | Minimum card width in px |
minCardHeight | number | 400 | Minimum card height in px |
gap | number | 16 | Gap between cards in px |
PaginationRenderProps
interface PaginationRenderProps {
currentPage: number
totalPages: number
totalItems: number
itemsPerPage: number
onPageChange: (page: number) => void
hasNextPage: boolean
hasPreviousPage: boolean
}renderItem Pattern
Each item is wrapped in a relative div filling its grid cell. Use absolute inset-0 on your card to fill it:
renderItem={(item) => (
<button className="absolute inset-0 rounded-xl bg-card border p-2 text-start">
{item.name}
</button>
)}