Skip to content
Redpanda UI
Esc
↑↓navigate↵open⌘Jpreview
Edit on GitHub

ListCard

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

Installation

bash
npx shadcn@latest add @redpanda/list-card
bash
pnpm dlx shadcn@latest add @redpanda/list-card
bash
yarn dlx shadcn@latest add @redpanda/list-card
bash
bunx shadcn@latest add @redpanda/list-card
bash
nubx shadcn@latest add @redpanda/list-card
bash
aube dlx shadcn@latest add @redpanda/list-card

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:

plaintext
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.

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

Recent changes

  • patchv1.2.0Pin shipped dependency floors to the version we develop against. Registry items now declare ranges like `^5.1.9` (the actual installed version) instead of collapsing to `^5.0.0`, so consumers start on the known-tested baseline while caret semantics still allow any compatible release within the same major.#133
  • minorv1.1.0Theme docs refresh, readability pass on semantic foregrounds, and consumer-facing Base UI regression fixes.#121
  • minorv1.0.0Post-Base-UI polish. Public API unchanged.#116
  • majorv1.0.0Migrate every Radix-based primitive to `@base-ui/react@^1.4.0` (Base UI).#114
  • minorv0.3.0Add theme-provider component to the registry with documentation and tests. Includes playground type improvements (export RegistryItem, remove as-const boilerplate) and docs site dark mode border color fix.#109
See full history →

Last updated on

Was this page helpful?