ListView
A CSS Grid-based horizontal layout for displaying items with start, intermediary, and end slots.
Made by eblairmckeeInstallation
Usage
The ListView component provides a consistent horizontal layout for displaying items in a list. It uses CSS Grid to create three distinct slots with different behavior:
import {
ListView,
ListViewGroup,
ListViewStart,
ListViewIntermediary,
ListViewEnd,
} from "@/components/redpanda-ui/list-view"Anatomy
The ListView component is built with a three-column grid system:
ListView Structure:
┌─────────────────────────────────────────────────────────────────────────┐
│ <ListViewGroup> (Container for multiple ListView items) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ <ListView> (Single row with 3-column grid) │ │
│ │ ┌──────────────────┬─────────────────────┬────────────────────────┐ │ │
│ │ │ <ListViewStart> │ <ListViewIntermed> │ <ListViewEnd> │ │ │
│ │ │ • Title │ • Badges │ • Single action │ │ │
│ │ │ • Description │ • Tags │ • Switch/Button │ │ │
│ │ │ • Metadata │ • Status indicators │ • Right-aligned │ │ │
│ │ │ │ • Hidden on mobile │ │ │ │
│ │ └──────────────────┴─────────────────────┴────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ <ListView> (Next row - border-top auto-applied in grouped) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
Grid Column Sizes:
• Col 1 (Start): minmax(40%, 2fr) - Primary content, shrinks gracefully
• Col 2 (Intermediary): 2fr - Secondary content, hidden below lg breakpoint
• Col 3 (End): 1fr - Actions, right-aligned, always visibleSlot Guidelines
ListViewStart (Column 1)
Purpose: Primary content - the main information users need to identify the item.
Best practices:
- Use
titleprop for the item name (auto-wrapped in Heading h3) - Use
descriptionprop for secondary info (auto-wrapped in muted Text) - Keep titles concise - they truncate on overflow
- Use custom children for complex layouts (icons + text)
// Simple: title and description props
<ListViewStart
title="Claude 3.5 Sonnet"
description="Anthropic's most intelligent model"
/>
// Complex: custom children
<ListViewStart>
<div className="flex items-center gap-2">
<AnthropicIcon className="size-4" />
<Heading level={4}>Claude 3.5 Sonnet</Heading>
</div>
<Text variant="muted">claude-3-5-sonnet-20241022</Text>
</ListViewStart>ListViewIntermediary (Column 2)
Purpose: Secondary metadata - badges, tags, status indicators that provide additional context.
Key behavior:
- Hidden below the
lgbreakpoint (1024px) - Left-aligned within its column
- Use for non-critical information that enhances the view
Best practices:
- Use BadgeGroup for multiple badges with overflow handling
- Keep content minimal - this column disappears on smaller screens
- Don't put critical actions here
<ListViewIntermediary gap="md">
<Badge variant="success-inverted">Active</Badge>
<BadgeGroup maxVisible={2}>
<Badge size="sm">React</Badge>
<Badge size="sm">TypeScript</Badge>
<Badge size="sm">Tailwind</Badge>
</BadgeGroup>
</ListViewIntermediary>ListViewEnd (Column 3)
Purpose: Single action - the primary interactive element for the item.
Common action patterns:
- Switch - Toggle enable/disable state
- Icon button (chevron) - Navigate to detail view
- Icon button (menu) - Open dropdown with multiple actions
// Toggle action
<ListViewEnd>
<Switch checked={enabled} onCheckedChange={setEnabled} />
</ListViewEnd>
// Navigate action
<ListViewEnd>
<Button size="icon-sm" variant="ghost">
<ChevronRightIcon className="size-4" />
</Button>
</ListViewEnd>
// Menu action
<ListViewEnd>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="icon-sm" variant="ghost">
<MoreHorizontalIcon className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Duplicate</DropdownMenuItem>
<DropdownMenuItem className="text-destructive">Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ListViewEnd>Interaction Patterns
ListView supports two mutually exclusive interaction patterns. Never nest pressable elements.
Pattern 1: Action in End Slot
The row displays information, with a single interactive element in the End slot.
<ListView>
<ListViewStart title="Model Name" description="Provider" />
<ListViewEnd>
<Switch checked={enabled} onCheckedChange={setEnabled} />
</ListViewEnd>
</ListView>Use when:
- Toggling a setting (Switch)
- Opening a menu with multiple actions (DropdownMenu)
- A specific action that doesn't navigate away
Pattern 2: Entire Row Clickable
The entire row is pressable and navigates somewhere. No interactive elements inside.
<ListView onClick={() => navigate(`/models/${id}`)}>
<ListViewStart title="Model Name" description="Click to view details" />
<ListViewEnd>
<ChevronRightIcon className="size-4 text-muted-foreground" />
</ListViewEnd>
</ListView>Use when:
- Navigating to a detail view
- Selecting an item from a list
- Opening a modal or panel
Key differences:
- Hover styles and cursor are automatic when
onClickis provided - 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 row
<ListView interactive onClick={handleRowClick}>
<ListViewStart title="Model" />
<ListViewEnd>
<Button onClick={handleEdit}>Edit</Button> {/* Nested pressable! */}
</ListViewEnd>
</ListView>This creates confusion about what happens when clicking different parts of the row.
Variants
Grouped (Default)
Use ListViewGroup to group multiple items with shared borders and automatic dividers:
<ListViewGroup variant="outlined">
<ListView>...</ListView>
<ListView>...</ListView>
<ListView>...</ListView>
</ListViewGroup>Standalone
For single items, use the outlined or elevated variant directly:
<ListView variant="outlined">
<ListViewStart title="Single Item" />
<ListViewEnd>
<Button>Action</Button>
</ListViewEnd>
</ListView>Interactive
When onClick is provided, the row automatically gets hover styles and a pointer cursor:
<ListView onClick={() => navigate('/details')}>
<ListViewStart title="Clickable Item" />
</ListView>You can also explicitly set interactive={true} to add hover styles without an onClick handler, or interactive={false} to disable hover styles even with an onClick.
Sizes
Control padding and gap with the size prop:
<ListView size="sm">Compact padding (px-4 py-3)</ListView>
<ListView size="md">Standard padding (px-5 py-4)</ListView>
<ListView size="lg">Generous padding (px-6 py-5)</ListView>Combining with ListCard
ListView and ListCard share the same data model, making it easy to offer users a choice between list and grid views.
Examples
Basic List
List/Card Toggle
Built by malinskibeniamin. The source code is available on GitHub.