Styled flexible list row with media, title, description, actions, header and footer.

Preview
Style
Default
Title Only
Title + Button
Title + Description

This is a description that provides additional context.

Media + Title + Description

This item includes media, title, and description.

Media + Title + Description + Button

Complete item with all components: media, title, description, and button.

Outline
Title Only
Title + Button
Title + Description

This is a description that provides additional context.

Media + Title + Description

This item includes media, title, and description.

Media + Title + Description + Button

Complete item with all components: media, title, description, and button.

Muted
Title Only
Title + Button
Title + Description

This is a description that provides additional context.

Media + Title + Description

This item includes media, title, and description.

Media + Title + Description + Button

Complete item with all components: media, title, description, and button.

Small
Title Only
Title + Button
Title + Description

This is a description that provides additional context.

Media + Title + Description

This item includes media, title, and description.

Media + Title + Description + Button

Complete item with all components: media, title, description, and button.

Extra Small
Title Only
Title + Button
Title + Description

This is a description that provides additional context.

Media + Title + Description

This item includes media, title, and description.

Media + Title + Description + Button

Complete item with all components: media, title, description, and button.

Outline — Small
Title Only
Title + Button
Title + Description

This is a description that provides additional context.

Media + Title + Description

This item includes media, title, and description.

Media + Title + Description + Button

Complete item with all components: media, title, description, and button.

ItemGroup
Item 1

First item in the group.

Item 2

Second item in the group.

Item 3

Third item in the group.

With Separator
Inbox

View all incoming messages.

Sent

View all sent messages.

Drafts

View all draft messages.

With Header & Footer
Design System
Component Library

A comprehensive collection of reusable UI components.

Updated 5 minutes ago
Your profile has been verified.
With Image
Avatar
shadcn

Creator of shadcn/ui — beautifully designed components.

src/demo/views/item.ts357 lines
import type { Html, HtmlBuilder } from 'foldkit/html'

import { button } from '../../generated/registry/ui/button'
import { Item } from '../../generated/registry/ui/item'
import { icon } from '../../generated/registry/lib/icons'
import { Archive, BadgeCheck, ChevronRight } from 'lucide'

import { defineSlice } from '../slice'
import type { Message, Model } from '../assemble'

const itemBlock = (
  variant: 'default' | 'outline' | 'muted',
  size: 'default' | 'sm' | 'xs',
  h: HtmlBuilder<Message>,
): ReadonlyArray<Html> => [
  Item<Message>(
    { variant, size },
    [Item.content<Message>({}, [Item.title<Message>({}, ['Title Only'], h)], h)],
    h,
  ),
  Item<Message>(
    { variant, size },
    [
      Item.content<Message>({}, [Item.title<Message>({}, ['Title + Button'], h)], h),
      Item.actions<Message>(
        {},
        [button<Message>({ variant: 'outline', size: 'sm' }, 'Action', h)],
        h,
      ),
    ],
    h,
  ),
  Item<Message>(
    { variant, size },
    [
      Item.content<Message>(
        {},
        [
          Item.title<Message>({}, ['Title + Description'], h),
          Item.description<Message>(
            {},
            ['This is a description that provides additional context.'],
            h,
          ),
        ],
        h,
      ),
    ],
    h,
  ),
  Item<Message>(
    { variant, size },
    [
      Item.media<Message>({ variant: 'icon' }, [icon(h, Archive, 'size-4')], h),
      Item.content<Message>(
        {},
        [
          Item.title<Message>({}, ['Media + Title + Description'], h),
          Item.description<Message>({}, ['This item includes media, title, and description.'], h),
        ],
        h,
      ),
    ],
    h,
  ),
  Item<Message>(
    { variant, size },
    [
      Item.media<Message>({ variant: 'icon' }, [icon(h, Archive, 'size-4')], h),
      Item.content<Message>(
        {},
        [
          Item.title<Message>({}, ['Media + Title + Description + Button'], h),
          Item.description<Message>(
            {},
            ['Complete item with all components: media, title, description, and button.'],
            h,
          ),
        ],
        h,
      ),
      Item.actions<Message>({}, [button<Message>({ size: 'sm' }, 'Action', h)], h),
    ],
    h,
  ),
]

export const itemView = (_model: Model, h: HtmlBuilder<Message>): Html =>
  h.div(
    [h.Class('flex w-full flex-col gap-8')],
    [
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Default']),
          h.div(
            [h.Class('flex w-full max-w-md flex-col gap-4')],
            itemBlock('default', 'default', h),
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Outline']),
          h.div(
            [h.Class('flex w-full max-w-md flex-col gap-4')],
            itemBlock('outline', 'default', h),
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Muted']),
          h.div([h.Class('flex w-full max-w-md flex-col gap-4')], itemBlock('muted', 'default', h)),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Small']),
          h.div([h.Class('flex w-full max-w-md flex-col gap-4')], itemBlock('default', 'sm', h)),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Extra Small']),
          h.div([h.Class('flex w-full max-w-md flex-col gap-4')], itemBlock('default', 'xs', h)),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Outline — Small']),
          h.div([h.Class('flex w-full max-w-md flex-col gap-4')], itemBlock('outline', 'sm', h)),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['ItemGroup']),
          Item.group<Message>(
            {},
            [
              Item<Message>(
                {},
                [
                  Item.content<Message>(
                    {},
                    [
                      Item.title<Message>({}, ['Item 1'], h),
                      Item.description<Message>({}, ['First item in the group.'], h),
                    ],
                    h,
                  ),
                ],
                h,
              ),
              Item<Message>(
                {},
                [
                  Item.content<Message>(
                    {},
                    [
                      Item.title<Message>({}, ['Item 2'], h),
                      Item.description<Message>({}, ['Second item in the group.'], h),
                    ],
                    h,
                  ),
                ],
                h,
              ),
              Item<Message>(
                {},
                [
                  Item.content<Message>(
                    {},
                    [
                      Item.title<Message>({}, ['Item 3'], h),
                      Item.description<Message>({}, ['Third item in the group.'], h),
                    ],
                    h,
                  ),
                ],
                h,
              ),
            ],
            h,
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['With Separator']),
          Item.group<Message>(
            {},
            [
              Item<Message>(
                { variant: 'outline' },
                [
                  Item.media<Message>({ variant: 'icon' }, [icon(h, Archive, 'size-4')], h),
                  Item.content<Message>(
                    {},
                    [
                      Item.title<Message>({}, ['Inbox'], h),
                      Item.description<Message>({}, ['View all incoming messages.'], h),
                    ],
                    h,
                  ),
                ],
                h,
              ),
              Item.separator<Message>({}, h),
              Item<Message>(
                { variant: 'outline' },
                [
                  Item.media<Message>({ variant: 'icon' }, [icon(h, Archive, 'size-4')], h),
                  Item.content<Message>(
                    {},
                    [
                      Item.title<Message>({}, ['Sent'], h),
                      Item.description<Message>({}, ['View all sent messages.'], h),
                    ],
                    h,
                  ),
                ],
                h,
              ),
              Item.separator<Message>({}, h),
              Item<Message>(
                { variant: 'outline' },
                [
                  Item.media<Message>({ variant: 'icon' }, [icon(h, Archive, 'size-4')], h),
                  Item.content<Message>(
                    {},
                    [
                      Item.title<Message>({}, ['Drafts'], h),
                      Item.description<Message>({}, ['View all draft messages.'], h),
                    ],
                    h,
                  ),
                ],
                h,
              ),
            ],
            h,
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div(
            [h.Class('px-1 text-xs font-medium text-muted-foreground')],
            ['With Header & Footer'],
          ),
          h.div(
            [h.Class('flex w-full max-w-md flex-col gap-4')],
            [
              Item<Message>(
                {},
                [
                  Item.header<Message>(
                    {},
                    [h.span([h.Class('text-sm font-medium')], ['Design System'])],
                    h,
                  ),
                  Item.content<Message>(
                    {},
                    [
                      Item.title<Message>({}, ['Component Library'], h),
                      Item.description<Message>(
                        {},
                        ['A comprehensive collection of reusable UI components.'],
                        h,
                      ),
                    ],
                    h,
                  ),
                  Item.footer<Message>(
                    {},
                    [h.span([h.Class('text-sm text-muted-foreground')], ['Updated 5 minutes ago'])],
                    h,
                  ),
                ],
                h,
              ),
              Item<Message>(
                { variant: 'outline' },
                [
                  Item.content<Message>(
                    {},
                    [Item.title<Message>({}, ['Your profile has been verified.'], h)],
                    h,
                  ),
                  Item.media<Message>({}, [icon(h, BadgeCheck, 'size-5')], h),
                  Item.actions<Message>({}, [icon(h, ChevronRight, 'size-4')], h),
                ],
                h,
              ),
            ],
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['With Image']),
          Item<Message>(
            { variant: 'outline' },
            [
              Item.media<Message>(
                { variant: 'image' },
                [
                  h.img([
                    h.Src('https://avatar.vercel.sh/shadcn'),
                    h.Alt('Avatar'),
                    h.Class('size-10 rounded-sm object-cover'),
                  ]),
                ],
                h,
              ),
              Item.content<Message>(
                {},
                [
                  Item.title<Message>({}, ['shadcn'], h),
                  Item.description<Message>(
                    {},
                    ['Creator of shadcn/ui — beautifully designed components.'],
                    h,
                  ),
                ],
                h,
              ),
              Item.actions<Message>(
                {},
                [button<Message>({ size: 'sm', variant: 'outline' }, 'Follow', h)],
                h,
              ),
            ],
            h,
          ),
        ],
      ),
    ],
  )

export const slice = defineSlice({
  fields: {},
  init: {},
  messages: [],
  handlers: () => ({}),
})

This code is shown verbatim — it’s the exact file used for this preview. Much of the surrounding wiring (slice, fields, messages, handlers) belongs to this site’s demo harness; you only need the view and the state that matters for your own app. Adapt what you need.

Installation

Add this component to your project:

pnpm dlx shadcn@latest add @foldcn/item

Source

The component ships as plain source — no build step, no wrapper. Copy it and make it yours.

registry/default/ui/item.ts213 lines
import type { Html, HtmlBuilder } from 'foldkit/html'

type Child = Html | string

import { cn } from '@/lib/utils'

export const itemGroupClass = 'gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2 group/item-group flex w-full flex-col'

export const itemSeparatorClass = 'my-2'

export const itemVariantKeys = ['default', 'outline', 'muted'] as const
export type ItemVariant = (typeof itemVariantKeys)[number]

export const itemVariants: Record<ItemVariant, string> = {
  default: 'border-transparent',
  outline: 'border-border',
  muted: 'bg-muted/50 border-transparent',
}

export const itemSizeKeys = ['default', 'sm', 'xs'] as const
export type ItemSize = (typeof itemSizeKeys)[number]

export const itemSizes: Record<ItemSize, string> = {
  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',
}

/** Upstream cva base string. */
export const itemClass =
  '[a]:hover:bg-muted rounded-lg border text-sm group/item flex w-full flex-wrap items-center transition-colors duration-100 outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [a]:transition-colors'

export const itemMediaVariantKeys = ['default', 'icon', 'image'] as const
export type ItemMediaVariant = (typeof itemMediaVariantKeys)[number]

export const itemMediaVariants: Record<ItemMediaVariant, string> = {
  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',
}

export const itemMediaClass =
  'gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start flex shrink-0 items-center justify-center [&_svg]:pointer-events-none'

export const itemContentClass =
  'gap-1 group-data-[size=xs]/item:gap-0 flex flex-1 flex-col [&+[data-slot=item-content]]:flex-none'

export const itemTitleClass = 'gap-2 text-sm leading-snug font-medium underline-offset-4 line-clamp-1 flex w-fit items-center'

export const itemDescriptionClass =
  'text-muted-foreground text-left text-sm leading-normal group-data-[size=xs]/item:text-xs line-clamp-2 font-normal [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary'

export const itemActionsClass = 'gap-2 flex items-center'
export const itemHeaderClass = 'gap-2 flex basis-full items-center justify-between'
export const itemFooterClass = 'gap-2 flex basis-full items-center justify-between'

type StyleConfig = Readonly<{ className?: string }>

type ItemConfig = Readonly<{
  variant?: ItemVariant
  size?: ItemSize
  className?: string
}>

type ItemMediaConfig = Readonly<{ variant?: ItemMediaVariant; className?: string }>

type ItemSeparatorConfig = Readonly<{
  orientation?: 'horizontal' | 'vertical'
  className?: string
}>

const itemGroup = <M>(
  config: StyleConfig,
  children: ReadonlyArray<Child>,
  h: HtmlBuilder<M>,
): Html =>
  h.div(
    [
      h.Role('list'),
      h.Class(cn(itemGroupClass, config.className)),
      h.DataAttribute('slot', 'item-group'),
    ],
    children,
  )

const itemSeparator = <M>(config: ItemSeparatorConfig, h: HtmlBuilder<M>): Html =>
  h.div(
    [
      h.Class(cn(itemSeparatorClass, config.className)),
      h.Role('separator'),
      h.AriaOrientation(config.orientation ?? 'horizontal'),
      h.DataAttribute('slot', 'item-separator'),
      h.DataAttribute('orientation', config.orientation ?? 'horizontal'),
      h.DataAttribute(config.orientation ?? 'horizontal', ''),
    ],
    [],
  )

const itemContainer = <M>(
  config: ItemConfig,
  children: ReadonlyArray<Child>,
  h: HtmlBuilder<M>,
): Html =>
  h.div(
    [
      h.Class(
        cn(
          itemClass,
          itemVariants[config.variant ?? 'default'],
          itemSizes[config.size ?? 'default'],
          config.className,
        ),
      ),
      h.DataAttribute('slot', 'item'),
      h.DataAttribute('variant', config.variant ?? 'default'),
      h.DataAttribute('size', config.size ?? 'default'),
    ],
    children,
  )

const itemMedia = <M>(
  config: ItemMediaConfig,
  children: ReadonlyArray<Child>,
  h: HtmlBuilder<M>,
): Html =>
  h.div(
    [
      h.Class(cn(itemMediaClass, itemMediaVariants[config.variant ?? 'default'], config.className)),
      h.DataAttribute('slot', 'item-media'),
      h.DataAttribute('variant', config.variant ?? 'default'),
    ],
    children,
  )

const itemContent = <M>(
  config: StyleConfig,
  children: ReadonlyArray<Child>,
  h: HtmlBuilder<M>,
): Html =>
  h.div(
    [h.Class(cn(itemContentClass, config.className)), h.DataAttribute('slot', 'item-content')],
    children,
  )

const itemTitle = <M>(
  config: StyleConfig,
  children: ReadonlyArray<Child>,
  h: HtmlBuilder<M>,
): Html =>
  h.div(
    [h.Class(cn(itemTitleClass, config.className)), h.DataAttribute('slot', 'item-title')],
    children,
  )

const itemDescription = <M>(
  config: StyleConfig,
  children: ReadonlyArray<Child>,
  h: HtmlBuilder<M>,
): Html =>
  h.p(
    [
      h.Class(cn(itemDescriptionClass, config.className)),
      h.DataAttribute('slot', 'item-description'),
    ],
    children,
  )

const itemActions = <M>(
  config: StyleConfig,
  children: ReadonlyArray<Child>,
  h: HtmlBuilder<M>,
): Html =>
  h.div(
    [h.Class(cn(itemActionsClass, config.className)), h.DataAttribute('slot', 'item-actions')],
    children,
  )

const itemHeader = <M>(
  config: StyleConfig,
  children: ReadonlyArray<Child>,
  h: HtmlBuilder<M>,
): Html =>
  h.div(
    [h.Class(cn(itemHeaderClass, config.className)), h.DataAttribute('slot', 'item-header')],
    children,
  )

const itemFooter = <M>(
  config: StyleConfig,
  children: ReadonlyArray<Child>,
  h: HtmlBuilder<M>,
): Html =>
  h.div(
    [h.Class(cn(itemFooterClass, config.className)), h.DataAttribute('slot', 'item-footer')],
    children,
  )

/** Styled item — a flexible list row with `Item.group`, `Item.separator`,
 *  `Item.media`, `Item.content`, `Item.title`, `Item.description`,
 *  `Item.actions`, `Item.header`, `Item.footer` sub-builders. Mirrors the
 *  shadcn v4 `item.tsx`. */
export const Item = Object.assign(itemContainer, {
  group: itemGroup,
  separator: itemSeparator,
  media: itemMedia,
  content: itemContent,
  title: itemTitle,
  description: itemDescription,
  actions: itemActions,
  header: itemHeader,
  footer: itemFooter,
})