Skip to content

Item

A flexible row that pairs media, a title and a description with actions. The building block of lists, settings and resource rows.

SourceRegistry
Connect a repository
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

Weekly digest
  1. 1MediaAn icon, avatar or image that identifies the item at a glance.
  2. 2ContainerA flexible row. Variants add a border or a muted fill; sizes set the density.
  3. 3TitleThe item's name. A long name wraps.
  4. 4DescriptionSupporting detail, clamped to two lines.
  5. 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.json

Usage

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.

default
outline
muted
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.

Quarterly report.pdf
default
Quarterly report.pdf
sm
Quarterly report.pdf
xs
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.

Icon
Blue Hour
AK
Aiko Kimura
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.

MC
Maya Chen
Owner
JO
Jonah Okafor
Admin
LP
Lena Park
Member
TR
Tomás Rivera
Invited
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>
  )
}

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>

ItemHeader and ItemFooter span the full width above and below the row — for progress, metadata or secondary actions.

Uploading 1 of 368%
design-tokens-v4.zip
x
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

Slack
DoOne visible action; the rest live in an overflow menu.
Slack
Don’tA row of equal buttons competes with the content it describes.

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 ItemMedia are decorative — the title carries the meaning. Informative images need alt text or role="img" with a label.

API reference

Item

Built on Base UI's useRender, so it accepts a render prop.

PropTypeDefault
variant

Transparent, raised tile, or recessed well.

"default" | "outline" | "muted""default"
size

Padding, gaps and media size.

"default" | "sm" | "xs""default"
render

Renders a different element — usually a link — with item styles.

ReactElement | (props, state) => ReactElementNo default

ItemMedia

PropTypeDefault
variant

Icon tile, cropped image, or unstyled slot for custom media.

"default" | "icon" | "image""default"

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.