---
title: ListCard
description: A slot-based card component for displaying items in a list or grid of cards.
icon: layout-grid
mode: wide
---

### Installation

```package-install
npx shadcn@latest add @redpanda/list-card
```

<ComponentPreview name="list-card-demo" />

## Usage

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.

```tsx
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 semantic `h3` if string)
- `end` - Trailing content (status badge, action icon)

```tsx
<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

```tsx
<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

```tsx
<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)

```tsx
<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

```tsx
// 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.

```tsx
<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.

```tsx
<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 `action` is provided
- The root is a nonnative Base UI button. Its descendants are presentational to assistive technology, so provide the meaningful `action.label` rather than relying on a nested heading
- `action.disabled` blocks activation and removes the card from sequential focus
- Set `action.pressed` only for controlled toggle selection; `onPress` does not change it locally
- Caller `onKeyDown` can cancel Enter with `preventDefault`; caller `onKeyUp` can 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:

```tsx expandable
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 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

<ComponentPreview name="list-card-demo" description="A grid of cards with headers, descriptions, and footers." />

### List/Card Toggle

See the ListView documentation for a complete example of toggling between list and card views.
