List Item
A row primitive with leading icon, title, subtitle, and trailing slot
Alice Chen
alice@demo.mercury.com
Anthony Buteo
anthony@demo.mercury.com
Jane Black
jane@demo.mercury.com
import { ChevronRight } from '@untitledui-pro/icons/solid';
import { Avatar } from '@/components/avatar';
import { Badge } from '@/components/badge';
import { List, ListItem } from '@/components/list-item';
import { Surface } from '@/components/surface';
export default function ListItemPreview() {
return (
<Surface elevation="default">
<List>
<ListItem interactive>
<ListItem.Leading>
<Avatar size="sm">
<Avatar.Fallback>AC</Avatar.Fallback>
</Avatar>
</ListItem.Leading>
<ListItem.Content>
<ListItem.Title>Alice Chen</ListItem.Title>
<ListItem.Subtitle>alice@demo.mercury.com</ListItem.Subtitle>
</ListItem.Content>
<ListItem.Trailing>
<Badge size="xs">Money Mover</Badge>
<ChevronRight className="size-4" />
</ListItem.Trailing>
</ListItem>
<ListItem interactive>
<ListItem.Leading>
<Avatar size="sm">
<Avatar.Fallback>AB</Avatar.Fallback>
</Avatar>
</ListItem.Leading>
<ListItem.Content>
<ListItem.Title>Anthony Buteo</ListItem.Title>
<ListItem.Subtitle>anthony@demo.mercury.com</ListItem.Subtitle>
</ListItem.Content>
<ListItem.Trailing>
<Badge size="xs" variant="neutral">
Read Only
</Badge>
<ChevronRight className="size-4" />
</ListItem.Trailing>
</ListItem>
<ListItem interactive active>
<ListItem.Leading>
<Avatar size="sm">
<Avatar.Fallback>JB</Avatar.Fallback>
</Avatar>
</ListItem.Leading>
<ListItem.Content>
<ListItem.Title>Jane Black</ListItem.Title>
<ListItem.Subtitle>jane@demo.mercury.com</ListItem.Subtitle>
</ListItem.Content>
<ListItem.Trailing>
<Badge size="xs">Admin</Badge>
<ChevronRight className="size-4" />
</ListItem.Trailing>
</ListItem>
</List>
</Surface>
);
} Dependencies
Source Code
import type { VariantProps } from 'cva';
import { Slot } from '@/components/slot';
import { cn, cva } from '@/lib/utils/classnames';
const listItemStyle = cva({
base: [
'group relative flex items-center text-left',
'transition duration-(--duration-hover) ease-(--ease)',
],
variants: {
interactive: {
true: 'focus-visible:ring-(length:--ring-width) cursor-pointer rounded-lg ring-ring hover:bg-foreground/4 focus-visible:outline-none active:scale-(--press-scale) motion-reduce:active:scale-100',
false: '',
},
active: {
true: 'bg-foreground/6',
false: '',
},
divided: {
true: 'border-(--color-divider) border-b last:border-b-0',
false: '',
},
size: {
sm: 'min-h-9 gap-2 px-2 py-1 text-sm',
md: 'min-h-11 gap-3 px-3 py-1.5 text-base',
lg: 'min-h-12 gap-3 px-3 py-2 text-base',
},
},
defaultVariants: {
interactive: false,
active: false,
divided: false,
size: 'md',
},
});
interface ListItemProps
extends React.ComponentPropsWithRef<'div'>,
VariantProps<typeof listItemStyle> {
asChild?: boolean;
}
const ListItem = ({
ref,
className,
interactive,
active,
divided,
size,
asChild,
...rest
}: ListItemProps) => {
const Comp = asChild ? Slot : 'div';
return (
<Comp
ref={ref}
data-active={active ? '' : undefined}
data-divided={divided ? '' : undefined}
className={cn(
listItemStyle({ interactive, active, divided, size }),
className
)}
{...rest}
/>
);
};
const ListItemLeading = ({
ref,
className,
...rest
}: React.ComponentPropsWithRef<'div'>) => (
<div
ref={ref}
aria-hidden
className={cn(
'flex shrink-0 items-center justify-center text-foreground-secondary',
className
)}
{...rest}
/>
);
const ListItemContent = ({
ref,
className,
...rest
}: React.ComponentPropsWithRef<'div'>) => (
<div
ref={ref}
className={cn('flex min-w-0 flex-1 flex-col justify-center', className)}
{...rest}
/>
);
const ListItemTitle = ({
ref,
className,
...rest
}: React.ComponentPropsWithRef<'div'>) => (
<div
ref={ref}
className={cn(
'truncate font-medium text-foreground leading-tight',
className
)}
{...rest}
/>
);
const ListItemSubtitle = ({
ref,
className,
...rest
}: React.ComponentPropsWithRef<'div'>) => (
<div
ref={ref}
className={cn(
'truncate text-foreground-secondary text-sm leading-tight',
className
)}
{...rest}
/>
);
const ListItemTrailing = ({
ref,
className,
...rest
}: React.ComponentPropsWithRef<'div'>) => (
<div
ref={ref}
className={cn(
'ml-auto flex shrink-0 items-center gap-2 text-foreground-secondary',
className
)}
{...rest}
/>
);
const List = ({
ref,
className,
...rest
}: React.ComponentPropsWithRef<'div'>) => (
<div
ref={ref}
className={cn(
'flex flex-col gap-0.5 has-[[data-divided]]:gap-0',
className
)}
{...rest}
/>
);
const CompoundListItem = Object.assign(ListItem, {
Leading: ListItemLeading,
Content: ListItemContent,
Title: ListItemTitle,
Subtitle: ListItemSubtitle,
Trailing: ListItemTrailing,
});
export type { ListItemProps };
export { CompoundListItem as ListItem, List, listItemStyle }; Server component. No
'use client'— list row layout compound; forwards props, no hooks or own handlers.
ListItem is the row primitive used everywhere in dashboards: account lists,
team rosters, transactions, integrations. It has a leading slot for an
avatar/icon, a content slot for title and subtitle, and a trailing slot for
amounts, badges, or chevrons.
Anatomy
<List>
<ListItem interactive>
<ListItem.Leading>{icon}</ListItem.Leading>
<ListItem.Content>
<ListItem.Title>{title}</ListItem.Title>
<ListItem.Subtitle>{subtitle}</ListItem.Subtitle>
</ListItem.Content>
<ListItem.Trailing>{trailing}</ListItem.Trailing>
</ListItem>
</List>
API Reference
ListItem
Extends the div element.
| Prop | Default | Type |
|---|---|---|
interactive | - | boolean |
active | - | boolean |
divided | - | boolean |
size | "md" | "sm""md""lg" |
asChild | - | boolean |
List
Extends the div element. Plain wrapper with role="list".
ListItem.Leading / Content / Title / Subtitle / Trailing
All extend the div element.
Examples
Default
Alice Chen
alice@demo.mercury.com
Anthony Buteo
anthony@demo.mercury.com
Jane Black
jane@demo.mercury.com
import { ChevronRight } from '@untitledui-pro/icons/solid';
import { Avatar } from '@/components/avatar';
import { Badge } from '@/components/badge';
import { List, ListItem } from '@/components/list-item';
import { Surface } from '@/components/surface';
export default function ListItemPreview() {
return (
<Surface elevation="default">
<List>
<ListItem interactive>
<ListItem.Leading>
<Avatar size="sm">
<Avatar.Fallback>AC</Avatar.Fallback>
</Avatar>
</ListItem.Leading>
<ListItem.Content>
<ListItem.Title>Alice Chen</ListItem.Title>
<ListItem.Subtitle>alice@demo.mercury.com</ListItem.Subtitle>
</ListItem.Content>
<ListItem.Trailing>
<Badge size="xs">Money Mover</Badge>
<ChevronRight className="size-4" />
</ListItem.Trailing>
</ListItem>
<ListItem interactive>
<ListItem.Leading>
<Avatar size="sm">
<Avatar.Fallback>AB</Avatar.Fallback>
</Avatar>
</ListItem.Leading>
<ListItem.Content>
<ListItem.Title>Anthony Buteo</ListItem.Title>
<ListItem.Subtitle>anthony@demo.mercury.com</ListItem.Subtitle>
</ListItem.Content>
<ListItem.Trailing>
<Badge size="xs" variant="neutral">
Read Only
</Badge>
<ChevronRight className="size-4" />
</ListItem.Trailing>
</ListItem>
<ListItem interactive active>
<ListItem.Leading>
<Avatar size="sm">
<Avatar.Fallback>JB</Avatar.Fallback>
</Avatar>
</ListItem.Leading>
<ListItem.Content>
<ListItem.Title>Jane Black</ListItem.Title>
<ListItem.Subtitle>jane@demo.mercury.com</ListItem.Subtitle>
</ListItem.Content>
<ListItem.Trailing>
<Badge size="xs">Admin</Badge>
<ChevronRight className="size-4" />
</ListItem.Trailing>
</ListItem>
</List>
</Surface>
);
} Previous
Kbd
Next
Listbox