ListCard
A slot-based card component for displaying items in a list or grid of cards.
Installation
Usage
The ListCard component provides a consistent card layout for displaying items in a grid or list of cards. It wraps the base Card component with specialized slots designed for list item display.
import {
ListCard,
ListCardHeader,
ListCardMeta,
ListCardBody,
ListCardDescription,
ListCardFooter,
} from "@/components/redpanda-ui/list-card"Anatomy
The ListCard component uses a vertical slot-based system:
ListCard Structure:
┌─────────────────────────────────────────────────────────────────────────┐
│ <ListCard> (Card container with outlined variant) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ <ListCardHeader> │ │
│ │ ┌──────────────┬────────────────────────────────┬─────────────────┐ │ │
│ │ │ start slot │ children (title) │ end slot │ │ │
│ │ │ • Icon │ • Auto-wrapped in Heading h3 │ • Status badge │ │ │
│ │ │ • Badge │ • Truncates on overflow │ • Icon │ │ │
│ │ └──────────────┴────────────────────────────────┴─────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ <ListCardMeta> (Optional - secondary info below header) │ │
│ │ • Provider name, model ID, dates │ │
│ │ • Auto-wrapped in small text styling │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ <ListCardBody> (Main content area) │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │
│ │ │ <ListCardDescription> (Optional - truncated text) │ │ │
│ │ │ • 1-3 line clamp │ │ │
│ │ │ • Auto-wrapped in muted Text │ │ │
│ │ └─────────────────────────────────────────────────────────────────┘ │ │
│ │ • Additional content (badges, stats, etc.) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ <ListCardFooter> (Border-top separator) │ │
│ │ ┌──────────────────────────────────┬──────────────────────────────┐ │ │
│ │ │ start slot │ end slot │ │ │
│ │ │ • Context info (badges) │ • Single action (Button) │ │ │
│ │ └──────────────────────────────────┴──────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘Slot Guidelines
ListCardHeader
Purpose: Identifies the card item with title and optional decorations.
Slots:
start- Leading content (icon, badge, avatar)children- Title text (auto-wrapped in Heading h3 if string)end- Trailing content (status badge, action icon)
<ListCardHeader
start={<Badge variant="success-inverted">Active</Badge>}
end={<Switch checked={true} />}
>
Claude 3.5 Sonnet
</ListCardHeader>ListCardMeta
Purpose: Secondary identification info shown below the header.
Best practices:
- Use for provider names, IDs, dates, categories
- Keep concise - truncates on overflow
- Strings auto-wrapped in small text styling
<ListCardMeta>Anthropic • claude-3-5-sonnet-20241022</ListCardMeta>
// Or with custom content
<ListCardMeta>
<span className="flex items-center gap-1">
<ClockIcon className="size-3" />
Updated 2 hours ago
</span>
</ListCardMeta>ListCardBody
Purpose: Main content area for description and additional information.
Best practices:
- Use ListCardDescription for truncated text descriptions
- Add BadgeGroup, stats, or other content below the description
- Controls vertical spacing between child elements
<ListCardBody>
<ListCardDescription lines={2}>
Anthropic's most intelligent model with state-of-the-art
reasoning and analysis capabilities.
</ListCardDescription>
<BadgeGroup maxVisible={3}>
<Badge size="sm">React</Badge>
<Badge size="sm">TypeScript</Badge>
<Badge size="sm">Tailwind</Badge>
</BadgeGroup>
</ListCardBody>ListCardDescription
Purpose: Truncated text description with line clamping.
Props:
lines- Number of lines before truncation (1, 2, or 3, default: 2)
<ListCardDescription lines={3}>
{longDescriptionText}
</ListCardDescription>ListCardFooter
Purpose: Actions and context info, separated by a border-top.
Slots:
start- Context info (badges, icons)children- Middle content (rarely used)end- Primary action or visual indicator
Common action patterns:
- Switch - Toggle enable/disable state
- Icon button (menu) - Open dropdown with multiple actions
- Visual indicator - Chevron icon when entire card is clickable
// Switch action
<ListCardFooter
start={<Badge size="sm">200K context</Badge>}
end={<Switch checked={enabled} onCheckedChange={setEnabled} />}
/>
// Menu action
<ListCardFooter
start={<Badge size="sm">128K context</Badge>}
end={
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="icon-sm" variant="ghost">
<MoreHorizontalIcon className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
}
/>Interaction Patterns
ListCard supports two mutually exclusive interaction patterns. Never nest pressable elements.
Pattern 1: Action in Footer
The card displays information, with a single interactive element in the footer.
<ListCard>
<ListCardHeader>Model Name</ListCardHeader>
<ListCardBody>
<ListCardDescription>Description text</ListCardDescription>
</ListCardBody>
<ListCardFooter end={<Switch checked={enabled} onCheckedChange={setEnabled} />} />
</ListCard>Use when:
- Toggling a setting (Switch)
- Opening a menu with multiple actions (DropdownMenu)
Pattern 2: Entire Card Clickable
The entire card is pressable and navigates somewhere. No interactive elements inside.
<ListCard onClick={() => navigate(`/models/${id}`)}>
<ListCardHeader end={<Badge>Active</Badge>}>Model Name</ListCardHeader>
<ListCardBody>
<ListCardDescription>Click to view details</ListCardDescription>
</ListCardBody>
<ListCardFooter end={<ChevronRightIcon className="size-4 text-muted-foreground" />} />
</ListCard>Use when:
- Navigating to a detail view
- Selecting an item from a grid
- Opening a modal or panel
Key differences:
- Hover styles and cursor are automatic when
onClickis provided - Footer end slot contains a visual indicator (icon), not a button
- The chevron is not wrapped in a Button - it's just an icon
Anti-pattern: Nested Pressables
Never do this:
// ❌ BAD: Button inside a clickable card
<ListCard onClick={handleCardClick}>
<ListCardHeader>Model</ListCardHeader>
<ListCardFooter end={<Button onClick={handleEdit}>Edit</Button>} />
</ListCard>This creates confusion about what happens when clicking different parts of the card.
Combining with ListView
ListCard and ListView are designed to display the same data in different formats. Create a shared data structure and render components for easy view switching:
type ModelItem = {
id: string;
name: string;
provider: string;
description: string;
status: 'active' | 'beta';
tags: string[];
};
// Same data, different views
function ModelCardView({ model }: { model: ModelItem }) {
return (
<ListCard>
<ListCardHeader end={<StatusBadge status={model.status} />}>
{model.name}
</ListCardHeader>
<ListCardMeta>{model.provider}</ListCardMeta>
<ListCardBody>
<ListCardDescription>{model.description}</ListCardDescription>
<BadgeGroup>
{model.tags.map((tag) => (
<Badge key={tag} size="sm">{tag}</Badge>
))}
</BadgeGroup>
</ListCardBody>
</ListCard>
);
}
function ModelListView({ model }: { model: ModelItem }) {
return (
<ListView>
<ListViewStart title={model.name} description={model.provider} />
<ListViewIntermediary>
<StatusBadge status={model.status} />
<BadgeGroup>
{model.tags.map((tag) => (
<Badge key={tag} size="sm">{tag}</Badge>
))}
</BadgeGroup>
</ListViewIntermediary>
</ListView>
);
}Examples
Basic Cards
List/Card Toggle
See the ListView documentation for a complete example of toggling between list and card views.
Built by malinskibeniamin. The source code is available on GitHub.