Item
A flexible row that pairs media, a title and a description with actions. The building block of lists, settings and resource rows.
import { GitBranchIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/components/ui/item"
export default function ItemDemo() {
return (
<Item variant="outline" className="max-w-sm">
<ItemMedia variant="icon">
<GitBranchIcon />
</ItemMedia>
<ItemContent>
<ItemTitle>Connect a repository</ItemTitle>
<ItemDescription>Deploy on every push to main.</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="sm" variant="outline">
Connect
</Button>
</ItemActions>
</Item>
)
}Anatomy
- 1MediaAn icon, avatar or image that identifies the item at a glance.
- 2ContainerA flexible row. Variants add a border or a muted fill; sizes set the density.
- 3TitleThe item's name. A long name wraps.
- 4DescriptionSupporting detail, clamped to two lines.
- 5ActionsButtons or a menu for this item, aligned to the end of the row.
Installation
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/item.jsonUsage
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/components/ui/item"<Item variant="outline">
<ItemMedia variant="icon">
<GitBranchIcon />
</ItemMedia>
<ItemContent>
<ItemTitle>Connect a repository</ItemTitle>
<ItemDescription>Deploy on every push to main.</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="sm" variant="outline">Connect</Button>
</ItemActions>
</Item>Examples
Variants
default blends into the surface it sits on, outline is a raised tile, and muted is a recessed well.
import { ShieldCheckIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/components/ui/item"
const variants = [
{
variant: "default",
note: "Blends into its surface. Lists inside cards and menus.",
},
{ variant: "outline", note: "A raised tile that stands alone on the page." },
{
variant: "muted",
note: "A recessed well for secondary or grouped content.",
},
] as const
export default function ItemVariants() {
return (
<div className="flex w-full max-w-md flex-col gap-4">
{variants.map(({ variant, note }) => (
<Item key={variant} variant={variant}>
<ItemMedia variant="icon">
<ShieldCheckIcon />
</ItemMedia>
<ItemContent>
<ItemTitle className="capitalize">{variant}</ItemTitle>
<ItemDescription>{note}</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="sm" variant="ghost">
Review
</Button>
</ItemActions>
</Item>
))}
</div>
)
}Sizes
default, sm and xs scale padding, gaps and media together. Use sm in lists inside cards and xs inside menus.
import { FileTextIcon } from "lucide-react"
import {
Item,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/components/ui/item"
export default function ItemSizes() {
return (
<div className="flex w-full max-w-md flex-col gap-3">
{(["default", "sm", "xs"] as const).map((size) => (
<Item key={size} variant="outline" size={size}>
<ItemMedia variant="icon">
<FileTextIcon />
</ItemMedia>
<ItemContent>
<ItemTitle>Quarterly report.pdf</ItemTitle>
<ItemDescription>
{size === "xs" ? "2.4 MB" : "2.4 MB · Updated 3 hours ago"}
</ItemDescription>
</ItemContent>
<span className="ml-auto font-mono text-xs text-muted-foreground">
{size}
</span>
</Item>
))}
</div>
)
}Media
ItemMedia holds an icon tile (variant="icon"), an image (variant="image"), or anything else — an Avatar, a file type badge. When the item has a description, media aligns to the top.
import { MusicIcon } from "lucide-react"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import {
Item,
ItemContent,
ItemDescription,
ItemGroup,
ItemMedia,
ItemTitle,
} from "@/components/ui/item"
export default function ItemMediaExample() {
return (
<ItemGroup className="max-w-sm gap-2">
<Item variant="outline" size="sm">
<ItemMedia variant="icon">
<MusicIcon />
</ItemMedia>
<ItemContent>
<ItemTitle>Icon</ItemTitle>
<ItemDescription>For objects and actions.</ItemDescription>
</ItemContent>
</Item>
<Item variant="outline" size="sm">
<ItemMedia variant="image">
<div
role="img"
aria-label="Album cover for Blue Hour"
className="size-full bg-brand-9"
/>
</ItemMedia>
<ItemContent>
<ItemTitle>Blue Hour</ItemTitle>
<ItemDescription>
Image — covers, thumbnails, previews.
</ItemDescription>
</ItemContent>
</Item>
<Item variant="outline" size="sm">
<ItemMedia>
<Avatar>
<AvatarFallback>AK</AvatarFallback>
</Avatar>
</ItemMedia>
<ItemContent>
<ItemTitle>Aiko Kimura</ItemTitle>
<ItemDescription>Avatar — people and teams.</ItemDescription>
</ItemContent>
</Item>
</ItemGroup>
)
}List
ItemGroup turns items into a list: the group gets role="list" and each plain item becomes a list item automatically. ItemSeparator draws hairlines between rows and stays silent for screen readers.
import * as React from "react"
import { MoreHorizontalIcon } from "lucide-react"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemGroup,
ItemMedia,
ItemSeparator,
ItemTitle,
} from "@/components/ui/item"
const people = [
{ name: "Maya Chen", email: "maya@northwind.dev", role: "Owner" },
{ name: "Jonah Okafor", email: "jonah@northwind.dev", role: "Admin" },
{ name: "Lena Park", email: "lena@northwind.dev", role: "Member" },
{ name: "Tomás Rivera", email: "tomas@northwind.dev", role: "Invited" },
]
export default function ItemList() {
return (
<div className="w-full max-w-md rounded-xl border bg-card p-1 shadow-raised">
<ItemGroup aria-label="Workspace members" className="gap-0">
{people.map((person, index) => (
<React.Fragment key={person.email}>
{index > 0 && <ItemSeparator className="my-0" />}
<Item size="sm">
<ItemMedia>
<Avatar>
<AvatarFallback>
{person.name
.split(" ")
.map((part) => part[0])
.join("")}
</AvatarFallback>
</Avatar>
</ItemMedia>
{/* min-w-0 lets long emails truncate instead of pushing the actions to a new line. */}
<ItemContent className="min-w-0 gap-0.5">
<ItemTitle>{person.name}</ItemTitle>
<ItemDescription className="truncate">
{person.email}
</ItemDescription>
</ItemContent>
<ItemActions>
{person.role === "Invited" ? (
<Badge variant="outline">Invited</Badge>
) : (
<span className="text-xs text-muted-foreground">
{person.role}
</span>
)}
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="ghost"
size="icon-sm"
aria-label={`Manage ${person.name}`}
/>
}
>
<MoreHorizontalIcon />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-44">
<DropdownMenuGroup>
<DropdownMenuItem>Change role</DropdownMenuItem>
<DropdownMenuItem>View activity</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem variant="destructive">
Remove
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</ItemActions>
</Item>
</React.Fragment>
))}
</ItemGroup>
</div>
)
}As a link
When the whole row navigates, render the item as a link with render. Hover and focus styles apply automatically. Wrap a set of links in a <nav> with a label rather than an ItemGroup — a link can't also be a list item.
import {
BookOpenIcon,
ChevronRightIcon,
HistoryIcon,
LayoutTemplateIcon,
} from "lucide-react"
import Link from "next/link"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/components/ui/item"
const resources = [
{
title: "Documentation",
description: "Guides, API reference and recipes.",
href: "/docs",
icon: BookOpenIcon,
},
{
title: "Examples",
description: "Complete screens built only from prfct.",
href: "/examples",
icon: LayoutTemplateIcon,
},
{
title: "Changelog",
description: "What changed in every release.",
href: "/docs/resources/changelog",
icon: HistoryIcon,
},
]
export default function ItemLink() {
return (
<nav aria-label="Resources" className="flex w-full max-w-md flex-col gap-2">
{resources.map(({ title, description, href, icon: Icon }) => (
<Item key={title} variant="outline" render={<Link href={href} />}>
<ItemMedia variant="icon">
<Icon />
</ItemMedia>
<ItemContent>
<ItemTitle>{title}</ItemTitle>
<ItemDescription>{description}</ItemDescription>
</ItemContent>
<ItemActions>
<ChevronRightIcon
className="size-4 text-muted-foreground"
aria-hidden="true"
/>
</ItemActions>
</Item>
))}
</nav>
)
}<Item variant="outline" render={<a href="/docs" />}>
…
</Item>Header and footer
ItemHeader and ItemFooter span the full width above and below the row — for progress, metadata or secondary actions.
import { FileArchiveIcon, XIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemFooter,
ItemHeader,
ItemMedia,
ItemTitle,
} from "@/components/ui/item"
import { Progress } from "@/components/ui/progress"
export default function ItemHeaderFooter() {
return (
<Item variant="outline" className="max-w-sm">
<ItemHeader>
<span className="text-xs font-medium text-muted-foreground">
Uploading 1 of 3
</span>
<span className="text-xs text-muted-foreground tabular-nums">68%</span>
</ItemHeader>
<ItemMedia variant="icon">
<FileArchiveIcon />
</ItemMedia>
<ItemContent>
<ItemTitle>design-tokens-v4.zip</ItemTitle>
<ItemDescription>12.8 MB of 18.9 MB · 6 s left</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="icon-sm" variant="ghost" aria-label="Cancel upload">
<XIcon />
</Button>
</ItemActions>
<ItemFooter>
<Progress
value={68}
aria-label="Upload progress"
className="w-full"
size="sm"
/>
</ItemFooter>
</Item>
)
}Guidelines
When to use
- For rows that describe one object with an optional action: a member, a file, an integration, a setting.
- For short lists where each row mixes media, text and controls.
When not to use
- For many rows compared across the same columns — use a Table.
- For a single, rich object — use a Card.
- For navigation menus with icons only — use a Sidebar or Dropdown Menu.
One primary action per row
Content
- Keep titles to one line and descriptions to two. A longer title wraps; a longer description is cut off after two lines.
- Put the most distinguishing information in the title. In a list of people, that's the name; in a list of files, the file name.
Accessibility
- List semantics are automatic. Inside
ItemGroup, plain items are exposed as list items. Rendered elements (links, buttons) keep their own roles. - Links read as one. A linked item's accessible name is its full text content. Keep titles and descriptions concise so the name stays useful.
- Label icon-only actions. Overflow buttons need a label that names the row: "Manage Maya Chen".
- Decorative media. Icons in
ItemMediaare decorative — the title carries the meaning. Informative images needalttext orrole="img"with a label.
API reference
Item
Built on Base UI's useRender, so it accepts a render prop.
ItemMedia
ItemGroup
A <div role="list"> that stacks items and tells them they are list items. Gaps tighten for smaller items.
ItemContent, ItemTitle, ItemDescription
The text column. A title shares its line with its icons or badges and wraps when it is too long; descriptions clamp to two lines.
ItemActions
Controls at the end of the row.
ItemHeader, ItemFooter
Full-width rows above and below the main row.
ItemSeparator
A horizontal rule between items, hidden from assistive technology inside a group.