Flexible row for lists: media, title, description and actions, with outline and muted variants.
Installation
pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/item.jsonInstalls to @/components/ui/item.
Manual installation
Install the dependencies:
pnpm add @base-ui/react@^1.8.0 class-variance-authority@^0.7.1Add the components it builds on: separator.
Copy the source into your project:
import { mergeProps } from '@base-ui/react/merge-props'
import { useRender } from '@base-ui/react/use-render'
import { cva, type VariantProps } from 'class-variance-authority'
import type * as React from 'react'
import { Separator } from '@/components/ui/separator'
import { cn } from '@/lib/utils'
function ItemGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
role="list"
data-slot="item-group"
className={cn(
'group/item-group flex w-full flex-col gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2',
className,
)}
{...props}
/>
)
}
function ItemSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {
return <Separator data-slot="item-separator" orientation="horizontal" className={cn('my-2', className)} {...props} />
}
const itemVariants = cva(
'group/item flex w-full flex-wrap items-center rounded-lg border text-sm transition-colors duration-100 outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [a]:transition-colors [a]:hover:bg-muted',
{
variants: {
variant: {
default: 'border-transparent',
outline: 'border-border',
muted: 'border-transparent bg-muted/50',
},
size: {
default: 'gap-2.5 px-3 py-2.5',
sm: 'gap-2.5 px-3 py-2.5',
xs: 'gap-2 px-2.5 py-2 in-data-[slot=dropdown-menu-content]:p-0',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
function Item({
className,
variant = 'default',
size = 'default',
render,
...props
}: useRender.ComponentProps<'div'> & VariantProps<typeof itemVariants>) {
return useRender({
defaultTagName: 'div',
props: mergeProps<'div'>(
{
className: cn(itemVariants({ variant, size, className })),
},
props,
),
render,
state: {
slot: 'item',
variant,
size,
},
})
}
const itemMediaVariants = cva(
'flex shrink-0 items-center justify-center gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start [&_svg]:pointer-events-none',
{
variants: {
variant: {
default: 'bg-transparent',
icon: "[&_svg:not([class*='size-'])]:size-4",
image:
'size-10 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover',
},
},
defaultVariants: {
variant: 'default',
},
},
)
function ItemMedia({
className,
variant = 'default',
...props
}: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>) {
return (
<div
data-slot="item-media"
data-variant={variant}
className={cn(itemMediaVariants({ variant, className }))}
{...props}
/>
)
}
function ItemContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-content"
className={cn(
'flex flex-1 flex-col gap-1 group-data-[size=xs]/item:gap-0 [&+[data-slot=item-content]]:flex-none',
className,
)}
{...props}
/>
)
}
function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-title"
className={cn(
'line-clamp-1 flex w-fit items-center gap-2 text-sm leading-snug font-medium underline-offset-4',
className,
)}
{...props}
/>
)
}
function ItemDescription({ className, ...props }: React.ComponentProps<'p'>) {
return (
<p
data-slot="item-description"
className={cn(
'line-clamp-2 text-left text-sm leading-normal font-normal text-muted-foreground group-data-[size=xs]/item:text-xs [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
className,
)}
{...props}
/>
)
}
function ItemActions({ className, ...props }: React.ComponentProps<'div'>) {
return <div data-slot="item-actions" className={cn('flex items-center gap-2', className)} {...props} />
}
function ItemHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-header"
className={cn('flex basis-full items-center justify-between gap-2', className)}
{...props}
/>
)
}
function ItemFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="item-footer"
className={cn('flex basis-full items-center justify-between gap-2', className)}
{...props}
/>
)
}
export {
Item,
ItemMedia,
ItemContent,
ItemActions,
ItemGroup,
ItemSeparator,
ItemTitle,
ItemDescription,
ItemHeader,
ItemFooter,
}A versatile flex container for displaying content such as list rows, document entries, people and settings. Items compose media, title, description, actions, header and footer slots, and can render as links via the render prop.
Features
- Variants: default, outline and muted
- Sizes: default, sm and xs
- Media slot with icon and image variants
- Actions, header and footer slots
- ItemGroup and ItemSeparator for lists
renderto render as a link or other element, e.g.render={<a href="…" />}
Usage
import { Button } from '@/components/ui/button'
import { Item, ItemActions, ItemContent, ItemDescription, ItemTitle } from '@/components/ui/item'
export function CaseItem() {
return (
<Item variant="outline">
<ItemContent>
<ItemTitle>Building permit, Storgatan 14</ItemTitle>
<ItemDescription>Case 2026-04812 · Registered 14 April 2026</ItemDescription>
</ItemContent>
<ItemActions>
<Button variant="outline" size="sm">Open</Button>
</ItemActions>
</Item>
)
}Examples
Default
An outlined case item with an action, and a small item rendered as a link.
Variants
The default, outline and muted variants, shown on council meetings.
Sizes
The default, sm and xs sizes.
With Icon
An item with icon media and an action.
With Avatar
A list of case officers to assign a case to.
With Image
ItemMedia variant="image" holds a thumbnail — here a token-based placeholder with an icon for each facility.
Group
A list of case documents with separators.
With Switch
Muted, small items as rows in a notification settings list.
With Header And Footer
A case card using header and footer slots.
As Link
E-services rendered as links.
Accessibility
- ItemGroup has role="list"; add role="listitem" to items when semantics matter
- Use
renderwith an anchor for navigable items so they are keyboard accessible - Give icon-only action buttons an aria-label
- Items show a visible focus ring when focusable