Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Item

A compact anatomy row for list/feed content — media, title, description, and actions, groupable with dividers.

Status
stable
Since
0.1.0
Accessibility pattern
list and listitem semantics

Last updated

New message

Ada Lovelace sent you a message.

Install

Add Item from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/item

The 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):

Item
ItemActions — data-slot="item-actions"
ItemContent — data-slot="item-content"
ItemDescription — data-slot="item-description"
ItemFooter — data-slot="item-footer"
ItemGroup — data-slot="item-group"
ItemHeader — data-slot="item-header"
ItemMedia — data-slot="item-media"
ItemSeparator — data-slot="item-separator"
ItemTitle — data-slot="item-title"

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"). Owns variant, size, and the render composition prop. Takes role="listitem" only inside an ItemGroup.
  • ItemMedia — the leading visual slot (data-slot="item-media"). Owns the variant chip 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 — a role="list" container of Item rows (data-slot="item-group").
  • ItemSeparator — a decorative divider between rows, wraps Separator (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.

variant=default

No surface — blends into the parent background.

variant=outline

A hairline border around the row.

variant=muted

A filled neutral wash — reads as a self-contained block.

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.

AL
Ada Lovelace

Default media — bare children (e.g. an Avatar).

Two-factor authentication

Icon media — a bordered chip around a lucide icon.

Q3 roadmap.pdf

Image media — a clipped square thumbnail tile.

Actions

ItemActions holds trailing controls — buttons, icon-buttons, badges — right-aligned in the row.

Q3-roadmap.pdf

2.4 MB · Uploaded 3 days ago

Shared

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.

AL
Ada Lovelace
2m ago

Approved the pull request.

GH
Grace Hopper
1h ago

Left a comment on the design doc.

Playground

Mix the row's surface, density, and media treatment, then copy the generated JSX.

New message

Ada Lovelace sent you a message.

<Item>
  <ItemMedia>
    <Mail />
  </ItemMedia>
  <ItemContent>
    <ItemTitle>New message</ItemTitle>
    <ItemDescription>Ada Lovelace sent you a message.</ItemDescription>
  </ItemContent>
</Item>

API Reference

Item

PropTypeDefaultDescription
renderuseRender.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).
sizeItemSize'md'Padding density. - md: roomy (standard row). - sm: compact (dense lists).
variantItemVariant'default'Surface treatment. - default: no surface, blends into the parent background. - outline: a hairline border around the row. - muted: a filled neutral wash.

ItemMedia

PropTypeDefaultDescription
variantItemMediaVariant'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

AttributeValues
data-slot"item-media"
data-variantmirrors a prop or state value

ItemSeparator

PropTypeDefaultDescription
decorativebooleantrueWhether 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

AttributeValues
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

  • ItemGroup is what licenses the row role: inside it, a non-interactive Item renders role="listitem" and pairs with the group's role="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. A listitem with no list parent is an axe aria-required-parent critical, which is why Timeline rows need no role plumbing of their own.
  • An interactive Item (render={<a />} or render={<button />}) keeps its native link / button role even inside a group — ARIA has no dual-role concept, and forcing listitem onto 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 a role="list" ItemGroup.
  • ItemSeparator is decorative (role="presentation", aria-hidden) — it never breaks the ItemGroup's list structure for assistive tech.
  • ItemMedia carries no independent semantics — an icon or image inside it should stay decorative; put the meaning in ItemTitle / ItemDescription.
  • Keyboard focus uses the centralized :focus-visible outline — no custom ring.
KeyAction
TabMove focus to the next interactive Item (or control inside ItemActions).
EnterActivate a focused link/button Item, or a focused action control.
SpaceActivate a focused button Item, or a focused action control.
ContractStates tested
Behaviourdefault
Accessibilitysemantic-html
Visualdefault, hover

Do / Don't

Do
Put the row's meaning in ItemTitle and ItemDescription — ItemMedia's icon or image stays decorative.
Don't
Make the whole row a link AND nest another link or button inside ItemActions — a control can't be both a landmark link and contain interactive descendants without confusing keyboard/AT navigation.

On this page