FoodEase

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.json

Usage

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-zero ResizeObserver measurement 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 × rows

The container is measured on mount and on every resize. Page 1 is always shown when items or container size changes.

API Reference

Props

PropTypeDefaultDescription
itemsT[][]Items to render
layout"grid" | "list""grid"Layout mode. "list" uses a single column
gridConfigGridConfigsee belowCard size constraints
renderItem(item: T) => ReactNodeRender function per item
renderEmpty() => ReactNodeRender function when items is empty
itemsPerPagenumberautoOverride auto page size
renderPagination(props: PaginationRenderProps) => ReactNodeCustom pagination UI
onItemVisible(item: T) => voidCalled for each visible item on page
classNamestringExtra classes on the outer wrapper
itemClassNamestringExtra classes on each item wrapper
getItemKey(item: T) => string | numberindexKey extractor

GridConfig

FieldTypeDefaultDescription
minCardWidthnumber320Minimum card width in px
minCardHeightnumber400Minimum card height in px
gapnumber16Gap 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>
)}

On this page