Resource Index
The resource index pattern is used for pages that display collections of items — lists of events, news articles, weather locations, transactions, or any data that users browse and filter.
Anatomy
A resource index page consists of:
- Page header — title, description, and primary action (e.g., “Create new”)
- Filter bar — search, category filters, sort options
- Content area — table or card grid displaying items
- Pagination — navigate between pages of results
- Empty state — displayed when no items match
Layout
<div className="mx-auto max-w-6xl px-4 py-8 sm:px-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="font-serif text-2xl font-bold">Events</h1>
<p className="text-sm text-muted-foreground">Browse upcoming events</p>
</div>
<Button>Create event</Button>
</div>
{/* Filters */}
<FilterBar className="mt-6" />
{/* Content */}
<div className="mt-6">
<DataTable columns={columns} data={events} />
</div>
{/* Pagination */}
<Pagination className="mt-6" />
</div>Table vs card grid
| Use table when | Use card grid when |
|---|---|
| Data has many comparable fields | Items are visually distinct |
| Users need to scan and compare | Items have images or rich content |
| Sorting by column is important | Mobile layout needs more flexibility |
| Data density is high | Items have varied content lengths |
Table layout
Use the DataTable component for structured data:
<DataTable
columns={[
{ accessorKey: "name", header: "Name" },
{ accessorKey: "date", header: "Date" },
{ accessorKey: "status", header: "Status" },
]}
data={items}
/>Card grid layout
Use a responsive grid for visual content:
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{items.map((item) => (
<Card key={item.id}>
<CardHeader>
<CardTitle>{item.name}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">{item.description}</p>
</CardContent>
</Card>
))}
</div>Filtering
Use the FilterBar component for consistent filtering across apps:
<FilterBar
filters={[
{ key: "category", label: "Category", options: categories },
{ key: "status", label: "Status", options: statuses },
]}
onFilterChange={handleFilterChange}
/>Pair with SearchBar for text search:
<div className="flex flex-col gap-4 sm:flex-row">
<SearchBar placeholder="Search events..." onSearch={handleSearch} className="flex-1" />
<FilterBar filters={filters} onFilterChange={handleFilterChange} />
</div>Empty states
Always provide a helpful empty state with the Empty component:
<Empty>
<EmptyHeader>
<EmptyMedia>
<Search className="size-12 text-muted-foreground" />
</EmptyMedia>
<EmptyTitle>No events found</EmptyTitle>
<EmptyDescription>
Try adjusting your filters or create a new event.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button>Create event</Button>
</EmptyContent>
</Empty>Loading states
Show skeletons that match the layout shape:
{/* Table skeleton */}
<div className="space-y-3">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-12 w-full" />
))}
</div>
{/* Card grid skeleton */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} className="h-48 w-full rounded-lg" />
))}
</div>Last updated on