Item
A compact anatomy row for list/feed content — media, title, description, and actions, groupable with dividers.
- Status
- Since
0.1.0- Accessibility pattern
- list and listitem semantics
Last updated
Install
Add Item from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/itemThe same command installs the registry items it composes: @vegastack/separator.
Usage
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/components/ui/item";
import { Mail } from "lucide-react";
<Item variant="outline">
<ItemMedia variant="icon">
<Mail />
</ItemMedia>
<ItemContent>
<ItemTitle>New message</ItemTitle>
<ItemDescription>Ada Lovelace sent you a message.</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="sm">View</Button>
</ItemActions>
</Item>;Anatomy
Item is a compound component. Every exported part, with the
data-slot it renders (generated from the canonical source):
Examples
Anatomy
Item is a compound component. Compose the parts inside the root, and group multiple rows in an
ItemGroup separated by ItemSeparator:
<ItemGroup>
<Item variant="default" size="md">
<ItemMedia variant="icon">{/* lucide icon */}</ItemMedia>
<ItemContent>
<ItemHeader>{/* title + trailing meta */}</ItemHeader>
<ItemTitle>Title</ItemTitle>
<ItemDescription>Supporting description text.</ItemDescription>
<ItemFooter>{/* secondary actions */}</ItemFooter>
</ItemContent>
<ItemActions>{/* buttons, badges */}</ItemActions>
</Item>
<ItemSeparator />
<Item>{/* … */}</Item>
</ItemGroup>Item— the row container (data-slot="item"). Ownsvariant,size, and therendercomposition prop. Takesrole="listitem"only inside anItemGroup.ItemMedia— the leading visual slot (data-slot="item-media"). Owns thevariantchip style.ItemContent— the flexible text column (data-slot="item-content").ItemTitle— the primary label (data-slot="item-title").ItemDescription— supporting body text, clamped to two lines (<p>,data-slot="item-description").ItemActions— a trailing row of controls (data-slot="item-actions").ItemHeader/ItemFooter— full-width top/bottom rows for multi-row layouts (data-slot="item-header"/"item-footer").ItemGroup— arole="list"container ofItemrows (data-slot="item-group").ItemSeparator— a decorative divider between rows, wrapsSeparator(data-slot="item-separator").
Variants
variant sets the row's surface — default (no surface, blends into the parent), outline (a
hairline border), or muted (a filled neutral wash). size sets the padding density — roomy
md or compact sm for dense lists.
Media
ItemMedia takes a variant: default renders children bare (compose an Avatar or any custom
glyph), icon wraps a single lucide-react icon in a bordered, muted square chip, and image
clips a thumbnail <img> into a rounded square tile. When the row also has an ItemDescription,
the media nudges down to align with the title instead of the row's vertical center.
Actions
ItemActions holds trailing controls — buttons, icon-buttons, badges — right-aligned in the row.
Interactive rows
Pass render to turn the whole row into a single focusable control — an <a> for navigation, a
<button> for an action. The row keeps the composed element's native link/button role instead
of taking listitem, so it stays fully operable and discoverable by assistive tech.
Groups & separators
Wrap rows in ItemGroup (role="list") and divide them with ItemSeparator — a decorative
Separator that never breaks the list's ARIA structure. Compose ItemHeader / ItemFooter
inside ItemContent for multi-row layouts like a comment or activity feed.
Playground
Mix the row's surface, density, and media treatment, then copy the generated JSX.
<Item>
<ItemMedia>
<Mail />
</ItemMedia>
<ItemContent>
<ItemTitle>New message</ItemTitle>
<ItemDescription>Ada Lovelace sent you a message.</ItemDescription>
</ItemContent>
</Item>API Reference
Item
| Prop | Type | Default | Description |
|---|---|---|---|
render | useRender.RenderProp<Record<string, unknown>> | — | Render the row as a different element (e.g. <a href="…" /> or <button />) via Base UI
render composition, making the whole row a single interactive/focusable control. Pass a
ReactElement or a render function. When set, the row keeps the composed element's native
link/button role instead of the default listitem role (see the role note below). |
size | ItemSize | 'md' | Padding density.
- md: roomy (standard row).
- sm: compact (dense lists). |
variant | ItemVariant | 'default' | Surface treatment.
- default: no surface, blends into the parent background.
- outline: a hairline border around the row.
- muted: a filled neutral wash. |
ItemMedia
| Prop | Type | Default | Description |
|---|---|---|---|
variant | ItemMediaVariant | 'default' | Visual treatment.
- default: bare children (default) — an Avatar, a custom glyph.
- icon: a bordered muted chip sized for a single lucide-react icon.
- image: a clipped square tile for a thumbnail <img>. |
Data attributes and CSS variables on ItemMedia
| Attribute | Values |
|---|---|
data-slot | "item-media" |
data-variant | mirrors a prop or state value |
ItemSeparator
| Prop | Type | Default | Description |
|---|---|---|---|
decorative | boolean | true | Whether the separator is purely visual. When true (the default) it is
hidden from assistive tech (role="presentation", aria-hidden) since the
surrounding layout already conveys the grouping. Set to false when the
divider carries semantic meaning (e.g. separating menu sections) so screen
readers announce it as a separator. |
orientation | "horizontal" | "vertical" | 'horizontal' | Axis the separator divides along. horizontal renders a 1px-tall full-width
rule; vertical renders a 1px-wide full-height rule. |
Data attributes and CSS variables on ItemSeparator
| Attribute | Values |
|---|---|
data-slot | "item-separator" |
ItemContent, ItemTitle, ItemDescription, ItemActions, ItemHeader,
ItemFooter, and ItemGroup add no props of their own — each accepts
everything its underlying element accepts (a <div>, or a <p> for
ItemDescription), plus className and ref.
Accessibility
ItemGroupis what licenses the row role: inside it, a non-interactiveItemrendersrole="listitem"and pairs with the group'srole="list", so assistive tech announces the row count and position.- A standalone
Item— one used on its own, or as the content of some other list's<li>— takes no ARIA role at all. Alistitemwith nolistparent is an axearia-required-parentcritical, which is whyTimelinerows need noroleplumbing of their own. - An interactive
Item(render={<a />}orrender={<button />}) keeps its nativelink/buttonrole even inside a group — ARIA has no dual-role concept, and forcinglistitemonto it would hide the interactive affordance. Compose interactive rows standalone, or alongside non-interactive rows in a group, rather than as the only children of arole="list"ItemGroup. ItemSeparatoris decorative (role="presentation",aria-hidden) — it never breaks theItemGroup's list structure for assistive tech.ItemMediacarries no independent semantics — an icon or image inside it should stay decorative; put the meaning inItemTitle/ItemDescription.- Keyboard focus uses the centralized
:focus-visibleoutline — no custom ring.
| Key | Action |
|---|---|
| Tab | Move focus to the next interactive Item (or control inside ItemActions). |
| Enter | Activate a focused link/button Item, or a focused action control. |
| Space | Activate a focused button Item, or a focused action control. |
| Contract | States tested |
|---|---|
| Behaviour | default |
| Accessibility | semantic-html |
| Visual | default, hover |