Redpanda UI
RC
Redpanda UI

ListCard

A slot-based card component for displaying items in a list or grid of cards.

Loading component...

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.

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 onClick is 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

Loading component...

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.

On this page