Item
A versatile component for displaying content with media, title, description, and actions.
A versatile component for displaying content with media, title, description, and actions.
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"import { Button } from "@/registry/acrylic/button"import { Item, ItemActions, ItemContent, ItemDescription, ItemMedia, ItemTitle,} from "@/registry/acrylic/item"export default function ItemDemo() { return ( <div className="flex w-full max-w-md flex-col gap-6"> <Item variant="outline"> <ItemContent> <ItemTitle>Basic Item</ItemTitle> <ItemDescription> A simple item with title and description. </ItemDescription> </ItemContent> <ItemActions> <Button variant="neutral" size="small"> Action </Button> </ItemActions> </Item> <Item variant="outline" size="sm" asChild> <a href="#"> <ItemMedia> <BadgeCheckIcon className="size-5" /> </ItemMedia> <ItemContent> <ItemTitle>Your profile has been verified.</ItemTitle> </ItemContent> <ItemActions> <ChevronRightIcon className="size-4" /> </ItemActions> </a> </Item> </div> )}The Item component is a straightforward flex container that can house nearly
any type of content. Use it to display a title, description, and actions. Group
it with the ItemGroup component to create a list of items.
Installation
npx shadcn add https://acrylic-ui.vercel.app/r/item.jsonUsage
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/components/acrylic/item"
import { Button } from "@/components/acrylic/button"
<Item>
<ItemMedia variant="icon">
<Icon />
</ItemMedia>
<ItemContent>
<ItemTitle>Title</ItemTitle>
<ItemDescription>Description</ItemDescription>
</ItemContent>
<ItemActions>
<Button>Action</Button>
</ItemActions>
</Item>Composition
Use the following components to compose an item.
ItemGroup
└── Item
├── ItemHeader
├── ItemMedia
├── ItemContent
│ ├── ItemTitle
│ └── ItemDescription
├── ItemActions
└── ItemFooterItem vs Field
Use Field if you need to display a form input such as a checkbox, input,
radio, or select.
If you only need to display content such as a title, description, and actions,
use Item.
Variant
Use the variant prop to change the visual style of the item.
import { Bell } from "lucide-react"import { Item, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle,} from "@/registry/acrylic/item"const variants = ["default", "outline", "muted"] as constexport default function ItemVariantsDemo() { return ( <ItemGroup className="w-full max-w-md"> {variants.map((variant) => ( <Item key={variant} variant={variant}> <ItemMedia variant="icon"> <Bell /> </ItemMedia> <ItemContent> <ItemTitle>{variant}</ItemTitle> <ItemDescription>Rendered with the {variant} item variant.</ItemDescription> </ItemContent> </Item> ))} </ItemGroup> )}Size
Use the size prop to change the size of the item.
import { FileText } from "lucide-react"import { Item, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle,} from "@/registry/acrylic/item"const sizes = ["default", "sm", "xs"] as constexport default function ItemSizesDemo() { return ( <ItemGroup className="w-full max-w-md"> {sizes.map((size) => ( <Item key={size} size={size} variant="outline"> <ItemMedia variant="icon"> <FileText /> </ItemMedia> <ItemContent> <ItemTitle>{size}</ItemTitle> <ItemDescription>Compact row geometry for {size} items.</ItemDescription> </ItemContent> </Item> ))} </ItemGroup> )}Examples
Icon
Use ItemMedia with variant="icon" to display an icon.
import { ShieldAlertIcon } from "lucide-react"import { Button } from "@/registry/acrylic/button"import { Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle,} from "@/registry/acrylic/item"export default function ItemActionsDemo() { return ( <ItemGroup className="w-full max-w-md"> <Item variant="outline"> <ItemMedia variant="icon"> <ShieldAlertIcon /> </ItemMedia> <ItemContent> <ItemTitle>Security Alert</ItemTitle> <ItemDescription>New login detected from unknown device.</ItemDescription> </ItemContent> <ItemActions> <Button variant="secondary" size="small">Review</Button> </ItemActions> </Item> </ItemGroup> )}Avatar
Use ItemMedia with variant="avatar" to display an avatar.
import { PlusIcon } from "lucide-react"import { Avatar, AvatarFallback } from "@/registry/acrylic/avatar"import { Button } from "@/registry/acrylic/button"import { Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle,} from "@/registry/acrylic/item"export default function ItemAvatarDemo() { return ( <ItemGroup className="w-full max-w-md"> <Item variant="outline"> <ItemMedia variant="avatar"> <Avatar> <AvatarFallback>ER</AvatarFallback> </Avatar> </ItemMedia> <ItemContent> <ItemTitle>Evil Rabbit</ItemTitle> <ItemDescription>Last seen 5 months ago</ItemDescription> </ItemContent> </Item> <Item variant="outline"> <ItemMedia variant="avatar" className="mr-1 w-[4.25rem] justify-start"> <div className="flex [&>[data-slot=avatar]+[data-slot=avatar]]:-ml-2"> <Avatar className="size-7 border border-[var(--acr-surface)]"> <AvatarFallback>CN</AvatarFallback> </Avatar> <Avatar className="size-7 border border-[var(--acr-surface)]"> <AvatarFallback>LR</AvatarFallback> </Avatar> <Avatar className="size-7 border border-[var(--acr-surface)]"> <AvatarFallback>ER</AvatarFallback> </Avatar> </div> </ItemMedia> <ItemContent> <ItemTitle>No Team Members</ItemTitle> <ItemDescription>Invite your team to collaborate on this project.</ItemDescription> </ItemContent> <ItemActions> <Button variant="secondary" size="small"> <PlusIcon /> Invite </Button> </ItemActions> </Item> </ItemGroup> )}Image
Use ItemMedia with variant="image" to display an image.
import { Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle,} from "@/registry/acrylic/item"const songs = [ { title: "Midnight City Lights", artist: "Electric Nights", album: "Neon Dreams", duration: "3:45", image: "https://images.unsplash.com/photo-1519608487953-e999c86e7455?q=80&w=200&auto=format&fit=crop", }, { title: "Coffee Shop Conversations", artist: "Urban Stories", album: "The Morning Brew", duration: "4:05", image: "https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?q=80&w=200&auto=format&fit=crop", }, { title: "Digital Rain", artist: "Binary Beats", album: "Cyber Symphony", duration: "3:30", image: "https://images.unsplash.com/photo-1518837695005-2083093ee35b?q=80&w=200&auto=format&fit=crop", },]export default function ItemImageDemo() { return ( <ItemGroup className="w-full max-w-md"> {songs.map((song) => ( <Item key={song.title} variant="outline"> <ItemMedia variant="image"> <img src={song.image} alt="" /> </ItemMedia> <ItemContent> <ItemTitle>{song.title}</ItemTitle> <ItemDescription> {song.artist} · {song.album} </ItemDescription> </ItemContent> <ItemActions> <span className="text-xs text-muted-foreground">{song.duration}</span> </ItemActions> </Item> ))} </ItemGroup> )}Post
Use Item to compose compact post rows with source metadata, a thumbnail, and
post actions.
"use client"import * as React from "react"import { BookmarkIcon, ExternalLinkIcon, MessageSquareIcon, MoreHorizontalIcon,} from "lucide-react"import { Avatar, AvatarFallback, AvatarImage } from "@/registry/acrylic/avatar"import { Badge } from "@/registry/acrylic/badge"import { Button } from "@/registry/acrylic/button"import { ButtonGroup, ButtonGroupItem, ButtonGroupSeparator,} from "@/registry/acrylic/button-group"import { Item, ItemActions, ItemContent, ItemDescription, ItemFooter, ItemGroup, ItemMedia, ItemMeta, ItemSeparator, ItemTitle,} from "@/registry/acrylic/item"const posts = { wide: { source: "Video", time: "12m", title: "A landscape media post in the stream", description: "Landscape media fills the content column and keeps a wide preview, matching the compact thumb treatment used for video and article covers.", comments: "24", images: [ "https://images.unsplash.com/photo-1497366754035-f200968a6e72?q=80&w=960&auto=format&fit=crop", ], }, portrait: { source: "Photo", time: "28m", title: "A portrait media post keeps its natural weight", description: "Vertical media stays left-aligned in the content column instead of stretching into a flat banner.", comments: "18", images: [ "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?q=80&w=720&auto=format&fit=crop", ], }, two: { source: "Gallery", time: "41m", title: "Two photos render as a balanced pair", description: "A two-image post uses a simple left-right split so both images stay readable at feed scale.", comments: "31", images: [ "https://images.unsplash.com/photo-1522199755839-a2bacb67c546?q=80&w=720&auto=format&fit=crop", "https://images.unsplash.com/photo-1498050108023-c5249f4df085?q=80&w=720&auto=format&fit=crop", ], }, multi: { source: "Gallery", time: "1h", title: "Many photos collapse into a compact grid", description: "Multi-image posts cap the visible grid and show the overflow count on the last tile.", comments: "57", images: [ "https://images.unsplash.com/photo-1483058712412-4245e9b90334?q=80&w=640&auto=format&fit=crop", "https://images.unsplash.com/photo-1518005020951-eccb494ad742?q=80&w=640&auto=format&fit=crop", "https://images.unsplash.com/photo-1497366811353-6870744d04b2?q=80&w=640&auto=format&fit=crop", "https://images.unsplash.com/photo-1515879218367-8466d910aaa4?q=80&w=640&auto=format&fit=crop", "https://images.unsplash.com/photo-1519389950473-47ba0277781c?q=80&w=640&auto=format&fit=crop", "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?q=80&w=640&auto=format&fit=crop", "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?q=80&w=640&auto=format&fit=crop", "https://images.unsplash.com/photo-1497215728101-856f4ea42174?q=80&w=640&auto=format&fit=crop", ], }, long: { source: "Text", time: "2h", title: "A long text post expands in place", description: "Stream keeps short text readable inside the row, but longer posts start collapsed so the feed remains scannable. Expanding the post reveals the full text without changing the surrounding item structure. This is useful for dense timelines where the user wants to skim several posts before opening one in detail.", comments: "12", images: [], },}const comments = [ { author: "Casey", handle: "casey", avatar: "https://github.com/maxleiter.png", body: "This layout keeps the media readable without turning the row into a full card.", time: "15m", location: "CA", replies: [ { author: "Nora", handle: "nora", avatar: "https://github.com/evilrabbit.png", body: "The inline expand is the right behavior for long text in a dense stream.", time: "11m", location: "NY", }, { author: "Mia", handle: "mia", avatar: "https://github.com/pacocoursey.png", body: "The gallery state feels close to the real stream. The comment rail should stay quiet like this.", time: "8m", location: "WA", }, ], }, { author: "Jordan", handle: "jordan", avatar: "https://github.com/rauchg.png", body: "Default Item works here as long as the nested surface does not add another panel.", time: "6m", location: "OR", }, { author: "Lee", handle: "lee", avatar: "https://github.com/leerob.png", body: "Moving the metadata under the comment helps the text keep its full measure.", time: "3m", location: "TX", },]const tabs = [ ["wide", "Wide"], ["portrait", "Tall"], ["two", "Two"], ["multi", "Grid"], ["long", "Text"],] as consttype PostKind = keyof typeof postsexport default function PostDemo() { const [kind, setKind] = React.useState<PostKind>("wide") const [expanded, setExpanded] = React.useState(false) const [commentsOpen, setCommentsOpen] = React.useState(true) const post = posts[kind] const isLong = kind === "long" const bodyExpanded = isLong && expanded return ( <div className="flex w-full max-w-xl flex-col items-start gap-3"> <ButtonGroup variant="segmented" size="small" value={kind} onValueChange={(value) => { setKind(value as PostKind) setExpanded(false) }} > {tabs.map(([value, label]) => ( <ButtonGroupItem key={value} value={value}> {label} </ButtonGroupItem> ))} </ButtonGroup> <ItemGroup className="w-full"> <Item variant="outline" className="items-start"> <ItemMedia variant="avatar"> <Avatar> <AvatarImage src="https://github.com/shadcn.png" alt="Acrylic Design" className="grayscale" /> <AvatarFallback>AD</AvatarFallback> </Avatar> </ItemMedia> <ItemContent className="flex flex-col gap-2"> <div className="flex items-center justify-between gap-3"> <div className="min-w-0"> <ItemTitle>Acrylic Design</ItemTitle> <ItemDescription>@acrylic-ui · {post.time}</ItemDescription> </div> <Badge variant="secondary" size="sm"> {post.source} </Badge> </div> <div> <ItemTitle className="text-[14px]">{post.title}</ItemTitle> <ItemDescription className={bodyExpanded ? "mt-1 line-clamp-none" : "mt-1 line-clamp-3"} > {post.description} </ItemDescription> {isLong ? ( <Button variant="ghost" size="small" className="px-0" onClick={() => setExpanded((value) => !value)} > {expanded ? "Collapse" : "Expand"} </Button> ) : null} <PostMedia kind={kind} images={post.images} /> </div> <ItemFooter className="pt-0"> <ButtonGroup size="small"> <Button variant="neutral" size="small" aria-pressed={commentsOpen} onClick={() => setCommentsOpen((value) => !value)} > <MessageSquareIcon /> {post.comments} </Button> <ButtonGroupSeparator /> <Button variant="neutral" size="small"> <BookmarkIcon /> Save </Button> <ButtonGroupSeparator /> <Button variant="neutral" size="small"> <ExternalLinkIcon /> Open </Button> </ButtonGroup> </ItemFooter> {commentsOpen ? ( <div className="flex flex-col"> <ItemSeparator /> {comments.map((comment) => ( <React.Fragment key={comment.handle}> <CommentItem comment={comment} /> {comment.replies?.map((reply) => ( <CommentItem key={reply.handle} comment={reply} reply /> ))} </React.Fragment> ))} </div> ) : null} </ItemContent> <ItemActions> <Button variant="ghost" size="medium" icon aria-label="More options"> <MoreHorizontalIcon /> </Button> </ItemActions> </Item> </ItemGroup> </div> )}type Comment = { author: string handle: string avatar: string body: string time: string location: string replies?: Comment[]}function CommentItem({ comment, reply = false,}: { comment: Comment reply?: boolean}) { return ( <Item variant="default" size="xs" className={reply ? "ml-10" : undefined}> <ItemMedia variant="avatar"> <Avatar> <AvatarImage src={comment.avatar} alt={comment.author} className="grayscale" /> <AvatarFallback>{comment.author.slice(0, 2).toUpperCase()}</AvatarFallback> </Avatar> </ItemMedia> <ItemContent> <ItemTitle> {comment.author}{" "} <span className="font-normal text-muted-foreground"> @{comment.handle} </span> </ItemTitle> <ItemDescription className="line-clamp-none text-foreground"> {comment.body} </ItemDescription> <ItemMeta> {comment.time} · {comment.location} </ItemMeta> </ItemContent> </Item> )}function PostMedia({ kind, images,}: { kind: PostKind images: string[]}) { if (images.length === 0) { return null } if (kind === "portrait") { return ( <div className="mt-2 w-[180px] overflow-hidden rounded-[12px] bg-[var(--acr-card-nested)]"> <img src={images[0]} alt="" className="aspect-[3/4] size-full object-cover" /> </div> ) } if (kind === "two") { return ( <div className="mt-2 grid max-w-md grid-cols-2 gap-1.5"> {images.map((image) => ( <div key={image} className="overflow-hidden rounded-[10px] bg-[var(--acr-card-nested)]" > <img src={image} alt="" className="aspect-[4/5] size-full object-cover" /> </div> ))} </div> ) } if (kind === "multi") { const shown = images.slice(0, 6) const overflow = images.length - shown.length return ( <div className="mt-2 grid max-w-md grid-cols-3 gap-1.5"> {shown.map((image, index) => { const showOverflow = index === shown.length - 1 && overflow > 0 return ( <div key={`${image}-${index}`} className="relative overflow-hidden rounded-[9px] bg-[var(--acr-card-nested)]" > <img src={image} alt="" className="aspect-square size-full object-cover" /> {showOverflow ? ( <span className="absolute inset-0 flex items-center justify-center bg-black/55 text-lg font-semibold text-white"> +{overflow} </span> ) : null} </div> ) })} </div> ) } return ( <div className="mt-2 max-w-md overflow-hidden rounded-[12px] bg-[var(--acr-card-nested)]"> <img src={images[0]} alt="" className="aspect-video size-full object-cover" /> </div> )}Text
Use ItemRow inside ItemContent to compose dense text previews with aligned
metadata and status actions.
"use client"import * as React from "react"import { MailIcon, StarIcon } from "lucide-react"import { ButtonGroup, ButtonGroupItem,} from "@/registry/acrylic/button-group"import { Item, ItemAction, ItemContent, ItemDescription, ItemGroup, ItemMeta, ItemRow, ItemSeparator, ItemTitle,} from "@/registry/acrylic/item"const messages = [ { from: "Acrylic Design", time: "9:41 AM", subject: "ItemRow now has a text-first example", preview: "Use ItemRow inside ItemContent when a compact preview needs aligned text, metadata, and a trailing status.", favorite: true, }, { from: "Design Systems", time: "Yesterday", subject: "Compact rows stay readable", preview: "Titles truncate independently from timestamps while the body keeps a calm two-line preview.", favorite: false, }, { from: "A very long sender name from the Acrylic component documentation team", time: "Monday", subject: "This subject line is intentionally long so ItemRow can show title truncation in a narrow text preview", preview: "This preview text is intentionally longer than the available narrow column width. It should clamp into a compact summary instead of stretching the row or pushing adjacent metadata out of view.", favorite: false, },]const modes = [ ["default", "Default"], ["narrow", "Narrow"], ["email", "Email list"],] as consttype TextMode = (typeof modes)[number][0]export default function ItemTextDemo() { const [mode, setMode] = React.useState<TextMode>("default") const isNarrow = mode === "narrow" const isEmailList = mode === "email" return ( <div className="flex w-full max-w-md flex-col items-center gap-3"> <ButtonGroup variant="segmented" size="small" value={mode} onValueChange={(value) => setMode(value as TextMode)} > {modes.map(([value, label]) => ( <ButtonGroupItem key={value} value={value}> {label} </ButtonGroupItem> ))} </ButtonGroup> <ItemGroup className={isNarrow ? "w-48" : "w-full"}> {messages.map((message, index) => ( <React.Fragment key={message.subject}> <Item variant={isEmailList ? "default" : "outline"} size="xs" selected={isEmailList && index === 0} asChild > <button type="button" className="w-full items-start px-3 py-2"> <ItemContent className="flex min-w-0 flex-col"> <ItemRow className="h-4 items-center"> <ItemTitle className="min-w-0 flex-1 text-[13px] font-semibold leading-4"> {message.from} </ItemTitle> <ItemMeta className="text-[11px] leading-4"> {message.time} </ItemMeta> </ItemRow> <ItemRow className="mt-1 h-[13px] items-center"> <ItemTitle className="min-w-0 flex-1 text-[11px] font-medium leading-[13px]"> {message.subject} </ItemTitle> <ItemAction className="h-3 w-3 text-muted-foreground [&>svg]:size-3"> {message.favorite ? <StarIcon /> : <MailIcon />} </ItemAction> </ItemRow> <ItemDescription className="mt-1 text-[11px] leading-[13px]"> {message.preview} </ItemDescription> </ItemContent> </button> </Item> {isEmailList && index < messages.length - 1 && index !== 0 ? ( <ItemSeparator className="mx-3" /> ) : null} </React.Fragment> ))} </ItemGroup> </div> )}Group
Use ItemGroup to group items together.
import * as React from "react"import { PlusIcon } from "lucide-react"import { Avatar, AvatarFallback, AvatarImage,} from "@/registry/acrylic/avatar"import { Button } from "@/registry/acrylic/button"import { Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle,} from "@/registry/acrylic/item"const people = [ { username: "shadcn", avatar: "https://github.com/shadcn.png", email: "shadcn@vercel.com", }, { username: "maxleiter", avatar: "https://github.com/maxleiter.png", email: "maxleiter@vercel.com", }, { username: "evilrabbit", avatar: "https://github.com/evilrabbit.png", email: "evilrabbit@vercel.com", },]export default function ItemGroupDemo() { return ( <ItemGroup className="max-w-sm"> {people.map((person) => ( <Item key={person.username} variant="outline"> <ItemMedia> <Avatar> <AvatarImage src={person.avatar} className="grayscale" /> <AvatarFallback>{person.username.charAt(0)}</AvatarFallback> </Avatar> </ItemMedia> <ItemContent className="gap-1"> <ItemTitle>{person.username}</ItemTitle> <ItemDescription>{person.email}</ItemDescription> </ItemContent> <ItemActions> <Button variant="ghost" size="medium" icon aria-label={`Invite ${person.username}`} > <PlusIcon /> </Button> </ItemActions> </Item> ))} </ItemGroup> )}Header
Use ItemHeader to add a header to an item.
import { Item, ItemContent, ItemDescription, ItemGroup, ItemHeader, ItemMedia, ItemTitle,} from "@/registry/acrylic/item"const models = [ { name: "v0-1.5-sm", description: "Everyday tasks and UI generation.", image: "https://images.unsplash.com/photo-1558655146-9f40138edfeb?q=80&w=240&auto=format&fit=crop", }, { name: "v0-1.5-lg", description: "Advanced thinking or reasoning.", image: "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?q=80&w=240&auto=format&fit=crop", }, { name: "v0-2.0-mini", description: "Open Source model for everyone.", image: "https://images.unsplash.com/photo-1518770660439-4636190af475?q=80&w=240&auto=format&fit=crop", },]export default function ItemHeaderDemo() { return ( <ItemGroup className="grid w-full max-w-3xl grid-cols-1 gap-3 sm:grid-cols-3"> {models.map((model) => ( <Item key={model.name} variant="outline"> <ItemHeader> <ItemMedia variant="image" className="h-28 w-full"> <img src={model.image} alt="" /> </ItemMedia> </ItemHeader> <ItemContent> <ItemTitle>{model.name}</ItemTitle> <ItemDescription>{model.description}</ItemDescription> </ItemContent> </Item> ))} </ItemGroup> )}Link
Use the asChild prop to render the item as a link. The focus state will be
applied to the anchor element.
import { ChevronRightIcon, ExternalLinkIcon } from "lucide-react"import { Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemTitle,} from "@/registry/acrylic/item"export default function ItemLinkDemo() { return ( <ItemGroup className="w-full max-w-md"> <Item asChild variant="outline"> <a href="https://ui.shadcn.com/docs" target="_blank" rel="noreferrer"> <ItemContent> <ItemTitle>Visit our documentation</ItemTitle> <ItemDescription>Learn how to get started with our components.</ItemDescription> </ItemContent> <ItemActions> <ExternalLinkIcon className="size-4 text-muted-foreground" /> <ChevronRightIcon className="size-4 text-muted-foreground" /> </ItemActions> </a> </Item> <Item asChild variant="outline"> <a href="https://ui.shadcn.com/docs/components/item" target="_blank" rel="noreferrer"> <ItemContent> <ItemTitle>External resource</ItemTitle> <ItemDescription>Opens in a new tab with security attributes.</ItemDescription> </ItemContent> <ItemActions> <ExternalLinkIcon className="size-4 text-muted-foreground" /> </ItemActions> </a> </Item> </ItemGroup> )}<Item asChild>
<a href="/dashboard">
<ItemMedia variant="icon">
<HomeIcon />
</ItemMedia>
<ItemContent>
<ItemTitle>Dashboard</ItemTitle>
<ItemDescription>Overview of your account and activity.</ItemDescription>
</ItemContent>
</a>
</Item>Dropdown
Use ItemActions to render a dropdown menu.
"use client"import { ChevronDownIcon } from "lucide-react"import { Button } from "@/registry/acrylic/button"import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,} from "@/registry/acrylic/dropdown-menu"import { Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemTitle,} from "@/registry/acrylic/item"export default function ItemDropdownDemo() { return ( <ItemGroup className="w-full max-w-md"> <Item variant="outline"> <ItemContent> <ItemTitle>Select</ItemTitle> <ItemDescription>Choose an option from the menu.</ItemDescription> </ItemContent> <ItemActions> <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" size="small"> Select <ChevronDownIcon /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuItem>Option One</DropdownMenuItem> <DropdownMenuItem>Option Two</DropdownMenuItem> <DropdownMenuItem>Option Three</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </ItemActions> </Item> </ItemGroup> )}RTL
To enable RTL support, add dir="rtl" to the item or item group.
"use client"import * as React from "react"import { BadgeCheckIcon } from "lucide-react"import { Button } from "@/registry/acrylic/button"import { Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle,} from "@/registry/acrylic/item"export default function ItemRtlDemo() { const [dir, setDir] = React.useState<"ltr" | "rtl">("rtl") return ( <ItemGroup className="w-full max-w-md" dir={dir}> <div className="mb-2 flex items-center justify-between gap-2 text-sm"> <span>Arabic (العربية)</span> <Button variant="secondary" size="small" onClick={() => setDir(dir === "rtl" ? "ltr" : "rtl")} > Toggle </Button> </div> <Item variant="outline"> <ItemMedia variant="icon"> <BadgeCheckIcon /> </ItemMedia> <ItemContent> <ItemTitle>عنصر أساسي</ItemTitle> <ItemDescription>عنصر بسيط يحتوي على عنوان ووصف.</ItemDescription> </ItemContent> <ItemActions> <Button variant="secondary" size="small">إجراء</Button> </ItemActions> </Item> </ItemGroup> )}API Reference
Item
The main item component.
| Prop | Type | Default |
|---|---|---|
variant | "default" | "outline" | "muted" | "default" |
size | "default" | "sm" | "xs" | "default" |
selected | boolean | false |
asChild | boolean | false |
<Item>
<ItemContent>
<ItemTitle>Item</ItemTitle>
<ItemDescription>Item description</ItemDescription>
</ItemContent>
</Item>Selected state
selected sets data-selected and paints the macOS key-window selection: a
solid bg-primary fill with a white label — the same language as a selected
TableRow. No className needed. Every part of the anatomy escalates itself
through the group-data-[selected=true]/item: hook on Item's root:
ItemTitle goes to text-primary-foreground, ItemDescription / ItemMeta /
ItemFooter / ItemActions to text-primary-foreground/80, and ItemMedia
(icon/image) swaps its nested wash for bg-white/15.
<Item asChild selected={active}>
<button type="button" onClick={() => setActive(id)}>
<ItemContent>
<ItemTitle>Title</ItemTitle>
<ItemDescription>Description</ItemDescription>
</ItemContent>
</button>
</Item>A nested Badge escalates on its own too (see
Badge). Only a child whose color you hard-coded
yourself needs a rule, and it reads the same hook:
group-data-[selected=true]/item:text-primary-foreground/80.
ItemGroup
Use ItemGroup to group items together.
<ItemGroup>
<Item />
<Item />
</ItemGroup>ItemSeparator
Use ItemSeparator to add a separator between items.
<ItemGroup>
<Item />
<ItemSeparator />
<Item />
</ItemGroup>ItemMedia
Use ItemMedia to display media content such as icons, images, or avatars.
| Prop | Type | Default |
|---|---|---|
variant | "default" | "icon" | "image" | "avatar" | "default" |
<ItemMedia variant="icon">
<Icon />
</ItemMedia><ItemMedia variant="image">
<img src="..." alt="..." />
</ItemMedia>ItemContent
Use ItemContent to wrap the title and description.
<ItemContent>
<ItemTitle>Title</ItemTitle>
<ItemDescription>Description</ItemDescription>
</ItemContent>ItemRow
Use ItemRow inside ItemContent to align title, metadata, or inline actions
on a single row.
<ItemRow>
<ItemTitle>Title</ItemTitle>
<ItemMeta>9:41 AM</ItemMeta>
</ItemRow>ItemTitle
Use ItemTitle to display the title.
<ItemTitle>Item Title</ItemTitle>ItemDescription
Use ItemDescription to display the description.
<ItemDescription>Item description</ItemDescription>ItemMeta
Use ItemMeta for compact secondary metadata such as timestamps, counts, or
short statuses.
<ItemMeta>Yesterday</ItemMeta>ItemActions
Use ItemActions to display actions.
<ItemActions>
<Button>Action</Button>
</ItemActions>ItemAction
Use ItemAction as a single-action alias for ItemActions, commonly inside
ItemRow.
<ItemAction>
<Icon />
</ItemAction>ItemHeader
Use ItemHeader to display a header.
<Item>
<ItemHeader>Header</ItemHeader>
<ItemContent>...</ItemContent>
</Item>ItemFooter
Use ItemFooter to display a footer.
<Item>
<ItemContent>...</ItemContent>
<ItemFooter>Footer</ItemFooter>
</Item>Acrylic differences
- Installation uses the Acrylic registry URL instead of
pnpm dlx shadcn@latest add item. - Imports use
@/components/acrylic/iteminstead of@/components/ui/item. - The structure, examples, and API reference follow the shadcn/ui Item docs.
- The component is styled with Acrylic tokens:
outlineuses the sameacr-frosted/--acr-surfacesurface as Card; nested, muted, and media surfaces use--acr-card-nested.