ListCard
A slot-based card component for displaying items in a list or grid of cards.
Installation
npx shadcn@latest add @redpanda/list-cardpnpm dlx shadcn@latest add @redpanda/list-cardyarn dlx shadcn@latest add @redpanda/list-cardbunx shadcn@latest add @redpanda/list-cardnubx shadcn@latest add @redpanda/list-cardaube dlx shadcn@latest add @redpanda/list-cardUsage
The ListCard component provides a consistent card layout for displaying items in a grid or list of cards. Passive cards compose Card. Action cards share its recipe and own their Base UI button semantics; their children must be noninteractive. Specialized native slots provide the list-item layout.
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 semantic 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 │ │ ││ │ │ • Body text with subtle ink │ │ ││ │ └─────────────────────────────────────────────────────────────────┘ │ ││ │ • 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 a semantich3if string)end- Trailing content (status badge, action icon)
<ListCardHeader start={ <Badge tone="success" variant="subtle"> Active </Badge> } end={<Switch aria-label="Enable model" 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} items={[ { id: 'React', textValue: 'React', content: <Badge size="sm">React</Badge>, }, { id: 'TypeScript', textValue: 'TypeScript', content: <Badge size="sm">TypeScript</Badge>, }, { id: 'Tailwind', textValue: 'Tailwind', content: <Badge size="sm">Tailwind</Badge>, }, ]} /></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 aria-label="Enable model" checked={enabled} onCheckedChange={setEnabled} />}/>
// Menu action<ListCardFooter start={<Badge size="sm">128K context</Badge>} end={ <DropdownMenu> <DropdownMenuTrigger render={ <Button aria-label="Model actions" size="icon-sm" variant="ghost"> <MoreHorizontalIcon className="size-4" /> </Button> } /> <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 aria-label="Enable model" 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 one action, such as selecting an item or opening a details panel. Its required action.label names that action. Do not put links, buttons, switches, menus, or interactive overflow inside it. For ordinary navigation, prefer a link within a passive card.
<ListCard action={{ label: `View ${model.name} details`, onPress: openDetails }}> <ListCardHeader end={<Badge>Active</Badge>}>Model Name</ListCardHeader> <ListCardBody> <ListCardDescription>Click to view details</ListCardDescription> </ListCardBody> <ListCardFooter end={<ChevronRightIcon className="size-4 text-subtle" />} /></ListCard>Use when:
- Opening a detail panel
- Selecting an item from a grid
- Opening a modal or panel
Key differences:
- Hover styles, cursor, focus ring and Enter/Space are automatic when
actionis provided - The root is a nonnative Base UI button. Its descendants are presentational to assistive technology, so provide the meaningful
action.labelrather than relying on a nested heading action.disabledblocks activation and removes the card from sequential focus- Set
action.pressedonly for controlled toggle selection;onPressdoes not change it locally - Caller
onKeyDowncan cancel Enter withpreventDefault; calleronKeyUpcan cancel Space - Footer end slot contains a visual indicator (icon), not a button
- The chevron is not wrapped in a Button - it’s just an icon
Avoid nested pressables
Action cards cannot contain other interactive descendants. TypeScript cannot inspect arbitrary React children, so this is an explicit composition contract, not a runtime traversal or nesting validator. Use a passive card whenever its content contains independent controls.
Breaking migration
Replace root onClick with action={{ label: 'Select model', onPress: selectModel }}. The callback no longer receives a DOM click event. Root role and tabIndex remain available for passive cards, but the action branch owns both. Existing native HTMLDivElement refs and keyboard handlers remain supported.
The action focus indicator is now the standard 3px ring rather than a 2px outline. Passive card appearance and footer controls are unchanged.
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 viewsfunction ModelCardView({ model }: { model: ModelItem }) { return ( <ListCard> <ListCardHeader end={ <StatusBadge variant={model.status === 'active' ? 'success' : 'informative'}> {model.status === 'active' ? 'Active' : 'Beta'} </StatusBadge> } > {model.name} </ListCardHeader> <ListCardMeta>{model.provider}</ListCardMeta> <ListCardBody> <ListCardDescription>{model.description}</ListCardDescription> <BadgeGroup items={model.tags.map((tag) => ({ id: tag, textValue: tag, content: <Badge size="sm">{tag}</Badge>, }))} /> </ListCardBody> </ListCard> );}
function ModelListView({ model }: { model: ModelItem }) { return ( <ListView> <ListViewStart title={model.name} description={model.provider} /> <ListViewIntermediary> <StatusBadge variant={model.status === 'active' ? 'success' : 'informative'}> {model.status === 'active' ? 'Active' : 'Beta'} </StatusBadge> <BadgeGroup items={model.tags.map((tag) => ({ id: tag, textValue: tag, content: <Badge size="sm">{tag}</Badge>, }))} /> </ListViewIntermediary> </ListView> );}Examples
Basic Cards
List/Card Toggle
See the ListView documentation for a complete example of toggling between list and card views.