A flexible list row with media, title, description and actions, in outline, muted and plain variants, that can also render as a link.
import { BellRing, ChevronRight, Plug, ShieldCheck } from "lucide-react";
import { Badge } from "@/components/ballmac/badge";
import { buttonVariants } from "@/components/ballmac/button";
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemGroup,
ItemMedia,
ItemTitle,
itemVariants,
} from "@/components/ballmac/item";
export default function ItemDemo() {
return (
<ItemGroup className="w-full max-w-md">
<Item variant="outline">
<ItemMedia variant="icon">
<ShieldCheck aria-hidden="true" />
</ItemMedia>
<ItemContent>
<ItemTitle>
Two-factor authentication <Badge status="success">On</Badge>
</ItemTitle>
<ItemDescription>
Codes from your authenticator app protect sign-in.
</ItemDescription>
</ItemContent>
<ItemActions>
<button type="button" className={buttonVariants({ variant: "outline", size: "sm" })}>
Manage
</button>
</ItemActions>
</Item>
<Item variant="outline">
<ItemMedia variant="icon">
<BellRing aria-hidden="true" />
</ItemMedia>
<ItemContent>
<ItemTitle>Deploy alerts</ItemTitle>
<ItemDescription>Email when a production deploy fails.</ItemDescription>
</ItemContent>
<ItemActions>
<button type="button" className={buttonVariants({ size: "sm" })}>
Enable
</button>
</ItemActions>
</Item>
<a href="#connections" className={itemVariants({ variant: "muted" })}>
<ItemMedia variant="icon">
<Plug aria-hidden="true" />
</ItemMedia>
<ItemContent>
<ItemTitle>Connected accounts</ItemTitle>
<ItemDescription>1 connected: GitHub</ItemDescription>
</ItemContent>
<ItemActions>
<ChevronRight aria-hidden="true" className="size-4 text-muted-foreground rtl:rotate-180" />
</ItemActions>
</a>
</ItemGroup>
);
}Installation
$ pnpm dlx shadcn@latest add @ballmac/itemInstall the dependencies.
$ pnpm add radix-ui class-variance-authorityCopy the source into your project.
components/ballmac/item.tsx// Ballmac UI: Item. https://ui.ballmac.com/components/item // Based on shadcn/ui Item (MIT, Copyright (c) 2023 shadcn), adding a compact size, an interactive hover state, and a list-aware group. import * as React from "react"; import { cva, type VariantProps } from "class-variance-authority"; import { Slot } from "radix-ui"; import { cn } from "@/lib/utils"; type ItemGroupProps = React.ComponentProps<"div">; /** A vertical stack of items. Pass `role="list"` here and `role="listitem"` on each non-link item when the rows are a real list. */ function ItemGroup({ className, ...props }: ItemGroupProps) { return ( <div data-slot="item-group" className={cn("flex w-full flex-col gap-2", className)} {...props} /> ); } type ItemSeparatorProps = React.ComponentProps<"div">; function ItemSeparator({ className, ...props }: ItemSeparatorProps) { return ( <div data-slot="item-separator" aria-hidden="true" className={cn("h-px w-full bg-border", className)} {...props} /> ); } const itemVariants = cva( "group/item flex w-full flex-wrap items-center rounded-lg border text-sm outline-none transition-colors duration-150 focus-visible:ring-[3px] focus-visible:ring-ring/50 [a&]:hover:bg-accent/60 [button&]:hover:bg-accent/60 [a&]:cursor-pointer [button&]:cursor-pointer [button&]:text-start", { variants: { variant: { default: "border-transparent", outline: "border-border bg-card", muted: "border-transparent bg-muted/50", }, size: { default: "gap-4 p-4", sm: "gap-3 px-3 py-2.5", }, }, defaultVariants: { variant: "default", size: "default" }, }, ); type ItemProps = React.ComponentProps<"div"> & VariantProps<typeof itemVariants> & { /** Render the child (a link or button) with item styles and behavior. */ asChild?: boolean; }; function Item({ className, variant = "default", size = "default", asChild = false, ...props }: ItemProps) { const Comp = asChild ? Slot.Root : "div"; return ( <Comp data-slot="item" data-variant={variant} data-size={size} className={cn(itemVariants({ variant, size }), className)} {...props} /> ); } const itemMediaVariants = cva( "flex shrink-0 items-center justify-center gap-2 [&_svg]:pointer-events-none", { variants: { variant: { default: "bg-transparent", icon: "size-9 rounded-lg border bg-muted [&_svg:not([class*='size-'])]:size-4", image: "size-10 overflow-hidden rounded-lg [&_img]:size-full [&_img]:object-cover", }, }, defaultVariants: { variant: "default" }, }, ); type ItemMediaProps = React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>; function ItemMedia({ className, variant = "default", ...props }: ItemMediaProps) { return ( <div data-slot="item-media" data-variant={variant} className={cn(itemMediaVariants({ variant }), className)} {...props} /> ); } type ItemContentProps = React.ComponentProps<"div">; function ItemContent({ className, ...props }: ItemContentProps) { return ( <div data-slot="item-content" className={cn("flex min-w-0 flex-1 flex-col gap-0.5", className)} {...props} /> ); } type ItemTitleProps = React.ComponentProps<"div">; function ItemTitle({ className, ...props }: ItemTitleProps) { return ( <div data-slot="item-title" className={cn("flex w-fit max-w-full items-center gap-2 text-sm leading-snug font-medium", className)} {...props} /> ); } type ItemDescriptionProps = React.ComponentProps<"p">; function ItemDescription({ className, ...props }: ItemDescriptionProps) { return ( <p data-slot="item-description" className={cn("line-clamp-2 text-sm leading-normal text-muted-foreground", className)} {...props} /> ); } type ItemActionsProps = React.ComponentProps<"div">; function ItemActions({ className, ...props }: ItemActionsProps) { return ( <div data-slot="item-actions" className={cn("flex shrink-0 items-center gap-2", className)} {...props} /> ); } type ItemHeaderProps = React.ComponentProps<"div">; function ItemHeader({ className, ...props }: ItemHeaderProps) { return ( <div data-slot="item-header" className={cn("flex basis-full items-center justify-between gap-2", className)} {...props} /> ); } type ItemFooterProps = React.ComponentProps<"div">; function ItemFooter({ className, ...props }: ItemFooterProps) { return ( <div data-slot="item-footer" className={cn("flex basis-full items-center justify-between gap-2", className)} {...props} /> ); } export { Item, ItemGroup, ItemSeparator, ItemMedia, ItemContent, ItemTitle, ItemDescription, ItemActions, ItemHeader, ItemFooter, itemVariants, type ItemProps, type ItemGroupProps, type ItemSeparatorProps, type ItemMediaProps, type ItemContentProps, type ItemTitleProps, type ItemDescriptionProps, type ItemActionsProps, type ItemHeaderProps, type ItemFooterProps, };Update the import paths to match your project setup.
Usage
import { Item, ItemGroup, ItemSeparator, ItemMedia, ItemContent, ItemTitle, ItemDescription, ItemActions, ItemHeader, ItemFooter } from "@/components/ballmac/item"The full example is in the Code tab above.
Examples
Files and footer
import { FileText } from "lucide-react";
import {
Item,
ItemContent,
ItemDescription,
ItemFooter,
ItemGroup,
ItemHeader,
ItemMedia,
ItemSeparator,
ItemTitle,
} from "@/components/ballmac/item";
const files = [
["Brand guidelines.pdf", "4.2 MB · edited 2 hours ago"],
["Launch checklist.md", "12 KB · edited yesterday"],
["Pricing model.xlsx", "88 KB · edited Monday"],
];
export default function ItemStates() {
return (
<div className="grid w-full max-w-md gap-4">
<ItemGroup className="gap-0 rounded-xl border">
{files.map(([name, meta], i) => (
<div key={name}>
{i > 0 && <ItemSeparator />}
<Item size="sm">
<ItemMedia>
<FileText aria-hidden="true" className="size-4 text-muted-foreground" />
</ItemMedia>
<ItemContent>
<ItemTitle>{name}</ItemTitle>
<ItemDescription className="line-clamp-1">{meta}</ItemDescription>
</ItemContent>
</Item>
</div>
))}
</ItemGroup>
<Item variant="outline" size="sm">
<ItemHeader>
<ItemTitle>Storage</ItemTitle>
<span className="text-xs text-muted-foreground tabular-nums">6.4 of 10 GB</span>
</ItemHeader>
<ItemContent>
<ItemDescription>Upgrade to keep uploading past your limit.</ItemDescription>
</ItemContent>
<ItemFooter>
<span className="text-xs text-muted-foreground">Resets monthly</span>
</ItemFooter>
</Item>
</div>
);
}API reference
<Item>
| Prop | Type | Default |
|---|---|---|
asChildRender the child (a link or button) with item styles and behavior. | boolean | false |
variant | "default" | "outline" | "muted" | "default" |
size | "default" | "sm" | "default" |
<ItemMedia>
| Prop | Type | Default |
|---|---|---|
variant | "default" | "icon" | "image" | "default" |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Tab / Enter | When rendered as a link or button, behaves as one |
Use with AI
Compose rows from ItemMedia, ItemContent, ItemTitle, ItemDescription and ItemActions. asChild styles a link or button as a whole-row target. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Item (@ballmac/item) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Settings, connected accounts and file lists
- Rows with an icon or avatar, text and an action
Not for
- Tabular data with columns; use table
- Long card layouts; use card
Registry JSON: https://ui.ballmac.com/r/item.json
Credits
Based on shadcn/ui Item, adapted by Ballmac. Free to use in personal and commercial projects.
- Registry
- shadcn/utils
Pairs well with
Badge
A small pill label in four variants with an optional status dot and success, warning and error tones. Style links with badgeVariants().
Avatar
A Radix avatar with image and initials fallback, three sizes, an optional presence dot, and AvatarGroup for overlapping stacks with a +N overflow counter.
Button
A button with six variants, three sizes, a pill shape and a built-in loading state. buttonVariants() styles links the same way.
Activity Feed
A compact actor and action feed with timestamps and an empty state.