Skip to Content
PatternsResource Index

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:

  1. Page header — title, description, and primary action (e.g., “Create new”)
  2. Filter bar — search, category filters, sort options
  3. Content area — table or card grid displaying items
  4. Pagination — navigate between pages of results
  5. 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 whenUse card grid when
Data has many comparable fieldsItems are visually distinct
Users need to scan and compareItems have images or rich content
Sorting by column is importantMobile layout needs more flexibility
Data density is highItems 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