A flexible list row with a leading media slot (icon box, avatar or picture), a title, a description and trailing actions. The whole row can be a link or a button while its actions stay separately clickable.
- Ada LovelaceEngineering lead
- Grace HopperPlatform
- Katherine JohnsonData
import { Avatar, Item, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle } from "@rdloom/react";
const people = [
{ name: "Ada Lovelace", role: "Engineering lead" },
{ name: "Grace Hopper", role: "Platform" },
{ name: "Katherine Johnson", role: "Data" },
];
export default function ItemBasicExample() {
return (
<div className="flex w-full justify-center">
<ItemGroup aria-label="Team" className="max-w-sm">
{people.map((p) => (
<Item key={p.name}>
<ItemMedia>
<Avatar name={p.name} decorative />
</ItemMedia>
<ItemContent>
<ItemTitle>{p.name}</ItemTitle>
<ItemDescription>{p.role}</ItemDescription>
</ItemContent>
</Item>
))}
</ItemGroup>
</div>
);
}Installation
npx rdloom add itemCopies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes. It needs react-aria-components; add --install to install them.
Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/item.jsonWorks, but without upgrade tracking.
Usage
import { Avatar, Item, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle } from "@rdloom/react";
<div className="flex w-full justify-center">
<ItemGroup aria-label="Team" className="max-w-sm">
{people.map((p) => (
<Item key={p.name}>
<ItemMedia>
<Avatar name={p.name} decorative />
</ItemMedia>
<ItemContent>
<ItemTitle>{p.name}</ItemTitle>
<ItemDescription>{p.role}</ItemDescription>
</ItemContent>
</Item>
))}
</ItemGroup>
</div>With icons and actions
- Q3 report.pdf2.4 MB, edited yesterday
- Brand guide.pdf8.1 MB, edited 3 days ago
import { Button, FileIcon, Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle } from "@rdloom/react";
const files = [
{ name: "Q3 report.pdf", meta: "2.4 MB, edited yesterday" },
{ name: "Brand guide.pdf", meta: "8.1 MB, edited 3 days ago" },
];
export default function ItemWithIconsAndActionsExample() {
return (
<div className="flex w-full justify-center">
<ItemGroup aria-label="Files" className="max-w-md gap-2">
{files.map((f) => (
<Item key={f.name} variant="outline">
<ItemMedia variant="icon">
<FileIcon />
</ItemMedia>
<ItemContent>
<ItemTitle>{f.name}</ItemTitle>
<ItemDescription>{f.meta}</ItemDescription>
</ItemContent>
<ItemActions>
<Button variant="secondary" size="sm">
Download
</Button>
</ItemActions>
</Item>
))}
</ItemGroup>
</div>
);
}As links
import { Button, ChevronRightIcon, HomeIcon, Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle, SettingsIcon } from "@rdloom/react";
export default function ItemAsLinksExample() {
return (
<div className="flex w-full justify-center">
<ItemGroup aria-label="Workspace" className="max-w-md gap-2">
<Item variant="outline" href="#overview">
<ItemMedia variant="icon">
<HomeIcon />
</ItemMedia>
<ItemContent>
<ItemTitle>Overview</ItemTitle>
<ItemDescription>Activity across all projects</ItemDescription>
</ItemContent>
<ItemActions>
<ChevronRightIcon />
</ItemActions>
</Item>
<Item variant="outline" href="#settings">
<ItemMedia variant="icon">
<SettingsIcon />
</ItemMedia>
<ItemContent>
<ItemTitle>Settings</ItemTitle>
<ItemDescription>Open the settings page, or reset it from here</ItemDescription>
</ItemContent>
<ItemActions>
<Button variant="ghost" size="sm">
Reset
</Button>
</ItemActions>
</Item>
</ItemGroup>
</div>
);
}Grouped
- Ada Lovelaceada@example.com
- Grace Hoppergrace@example.com
- Katherine Johnsonkatherine@example.com
import { Fragment } from "react";
import { Avatar, Item, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemSeparator, ItemTitle } from "@rdloom/react";
const people = [
{ name: "Ada Lovelace", email: "ada@example.com" },
{ name: "Grace Hopper", email: "grace@example.com" },
{ name: "Katherine Johnson", email: "katherine@example.com" },
];
export default function ItemGroupedExample() {
return (
<div className="flex w-full justify-center">
<ItemGroup aria-label="Members" className="max-w-sm">
{people.map((p, i) => (
<Fragment key={p.email}>
{i > 0 ? <ItemSeparator /> : null}
<Item size="sm">
<ItemMedia>
<Avatar name={p.name} size="sm" decorative />
</ItemMedia>
<ItemContent>
<ItemTitle>{p.name}</ItemTitle>
<ItemDescription>{p.email}</ItemDescription>
</ItemContent>
</Item>
</Fragment>
))}
</ItemGroup>
</div>
);
}Muted
import { InfoIcon, Item, ItemContent, ItemDescription, ItemMedia, ItemTitle } from "@rdloom/react";
export default function ItemMutedExample() {
return (
<div className="flex w-full justify-center">
<Item variant="muted" className="max-w-md">
<ItemMedia variant="icon">
<InfoIcon />
</ItemMedia>
<ItemContent>
<ItemTitle>Billing moves to the first of the month</ItemTitle>
<ItemDescription>Your next invoice covers the days from today until then.</ItemDescription>
</ItemContent>
</Item>
</div>
);
}API Reference
Defined by the spec. Components also accept the props of the React Aria component they wrap.
| Prop | Type | Default |
|---|---|---|
childrenrequiredItemMedia, ItemContent and ItemActions. ItemActions is placed outside the link or button so it never nests one interactive element in another. | node | none |
variantdefault has no surface, outline adds a border, muted adds a tinted background. | "default" | "outline" | "muted" | "default" |
sizesm is a compact row, md the standard row. | "sm" | "md" | "md" |
hrefMakes the row a link to this address. | string | none |
onPressMakes the row a button, or runs on a press of the link when href is also set. | (event: PressEvent) => void | none |
isDisabledDisables the link or button part of the row. Actions keep their own state. | boolean | false |
Accessibility
Role listitem, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.
Keyboard
- Tab moves to the row when it is a link or button, then to each action
- Enter activates a link row; Enter and Space activate a button row
Screen readers announce
- A group is announced as a list with the number of items
- A link row is announced as a link with its title and description
- Each action is announced by its own label
What your code must do
- ItemGroup is a list (role list) and each Item inside it is a listitem
- A link or button row has a visible focus ring and a hover state
- Actions are siblings of the link or button, never children of it, so no interactive element is nested in another
- The link or button is named by its title and description; icon-only actions need a label
- ItemMedia icons are decorative: the title carries the meaning
- ItemSeparator is decorative inside a list and does not break the list count
Guidelines
Use it when
- Lists of people, files, settings or notifications with an icon, text and an action
- Rows that open a detail page
- A compact stack of related options
Avoid it when
- Rows of the same fields to compare: use a table or data grid
- A large selectable collection: use a list box or grid list
Don't
- Putting a button or link inside ItemContent when the row is already a link
- Icon-only actions without a label
- Using the muted variant for every row so none stands out
Design tokens
The semantic tokens this component uses. Change them once and every component follows; see Design tokens.
--rd-color-text-default--rd-color-text-muted--rd-color-border-default--rd-color-surface-subtle--rd-color-focus-ring--rd-radius-control