Glrk UI

Item

A simplified wrapper for Shadcn Item with flexible option handling and automatic type conversion

Product
Wireless Headphones

Noise-cancelling over-ear headphones with long battery life.

Perfect for work, travel, and everyday listening. Supports fast charging.
Updated 2 days ago
Smart Watch

Track your fitness, heart rate and notifications.

In Stock
Product
Wireless Headphones

Noise-cancelling over-ear headphones with long battery life.

Perfect for work, travel, and everyday listening. Supports fast charging.
Updated 2 days ago

Installation

npx shadcn@latest add @glrk-ui/item

If you haven't set up the prerequisites yet, check out Prerequest section.

Copy and paste the following code into your project.

ui/item.tsx
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { mergeProps } from '@base-ui/react/merge-props'
import { useRender } from '@base-ui/react/use-render'

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

import { Separator } from '@/components/ui/separator'

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=menu-content]:p-0',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  },
)

type itemDivProps = useRender.ComponentProps<'div'> & VariantProps<typeof itemVariants>
function Item({
  className,
  variant = 'default',
  size = 'default',
  render,
  ...props
}: itemDivProps) {
  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',
    },
  },
)

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}
    />
  )
}

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}
    />
  )
}

type commonCls = {
  itemWrapperCls?: string
  headerCls?: string
  titleCls?: string
  mediaCls?: string
  descriptionCls?: string
  contentCls?: string
  actionsCls?: string
  footerCls?: string
  itemProps?: Omit<itemDivProps, 'className'>
  itemMediaProps?: Omit<itemMediaProps, 'className'>
}

type itemT = commonCls & {
  header?: React.ReactNode
  title?: React.ReactNode
  media?: React.ReactNode
  description?: React.ReactNode
  actions?: React.ReactNode
  content?: React.ReactNode
  footer?: React.ReactNode
}

type itemsT = itemT[]

function ItemWrapper({
  title,
  description,
  actions,
  content,
  footer,
  media,
  header,
  itemWrapperCls,
  headerCls,
  titleCls,
  descriptionCls,
  contentCls,
  mediaCls,
  actionsCls,
  footerCls,
  itemProps,
  itemMediaProps,
}: itemT) {
  return (
    <Item {...itemProps} className={cn(itemWrapperCls)}>
      {media && (
        <ItemMedia {...itemMediaProps} className={cn(mediaCls)}>
          {media}
        </ItemMedia>
      )}
      {header && <ItemHeader className={cn(headerCls)}>{header}</ItemHeader>}

      {(title || description || content) && (
        <ItemContent className={cn(contentCls)}>
          {title && <ItemTitle className={cn(titleCls)}>{title}</ItemTitle>}
          {description && (
            <ItemDescription className={cn(descriptionCls)}>{description}</ItemDescription>
          )}
          {content}
        </ItemContent>
      )}

      {actions && <ItemActions className={cn(actionsCls)}>{actions}</ItemActions>}
      {footer && <ItemFooter className={cn(footerCls)}>{footer}</ItemFooter>}
    </Item>
  )
}

type itemGroupProps = commonCls & {
  wrapperCls?: string
  items: itemsT
}

function ItemGroupWrapper({
  wrapperCls,
  items,
  itemWrapperCls,
  headerCls,
  titleCls,
  descriptionCls,
  contentCls,
  mediaCls,
  actionsCls,
  footerCls,
  itemProps,
  itemMediaProps,
}: itemGroupProps) {
  return (
    <ItemGroup className={wrapperCls}>
      {items.map((item, i) => (
        <ItemWrapper
          key={i}
          {...item}
          itemWrapperCls={cn(itemWrapperCls, item.itemWrapperCls)}
          headerCls={cn(headerCls, item.headerCls)}
          titleCls={cn(titleCls, item.titleCls)}
          descriptionCls={cn(descriptionCls, item.descriptionCls)}
          contentCls={cn(contentCls, item.contentCls)}
          mediaCls={cn(mediaCls, item.mediaCls)}
          actionsCls={cn(actionsCls, item.actionsCls)}
          footerCls={cn(footerCls, item.footerCls)}
          itemProps={{ ...itemProps, ...item.itemProps }}
          itemMediaProps={{ ...itemMediaProps, ...item.itemMediaProps }}
        />
      ))}
    </ItemGroup>
  )
}

export {
  Item,
  ItemMedia,
  ItemContent,
  ItemActions,
  ItemGroup,
  ItemSeparator,
  ItemTitle,
  ItemDescription,
  ItemHeader,
  ItemFooter,
  ItemWrapper,
  ItemGroupWrapper,
  type itemT,
  type itemsT,
}

Usage

Basic

import { ItemWrapper } from "@/components/ui/item"
import { Button } from "@/components/ui/button"

export function Basic() {
  return (
    <ItemWrapper
      header="Product"
      title="Wireless Headphones"
      description="Noise-cancelling over-ear headphones with long battery life."
      content="Perfect for work, travel, and everyday listening. Supports fast charging."
      actions={<Button size="sm">Buy Now</Button>}
      footer="Updated 2 days ago"
    />
  )
}

Item Group

import { type itemsT, ItemGroupWrapper } from "@/components/ui/item"
import { Button } from "@/components/ui/button"

export function Group() {
   const items: itemsT = [
    {
      header: "Product",
      title: "Wireless Headphones",
      description: "Noise-cancelling over-ear headphones with long battery life.",
      content: "Perfect for work, travel, and everyday listening. Supports fast charging.",
      actions: <Button size="sm">Buy Now</Button>,
      footer: "Updated 2 days ago",
      itemProps: { variant: "outline" }
    },
    {
      title: "Smart Watch",
      description: "Track your fitness, heart rate and notifications.",
      actions: <Button variant="outline" size="sm">View Details</Button>,
      footer: "In Stock",
      itemProps: { variant: "muted" }
    },
  ]

  return (
    <ItemGroupWrapper
      items={items}
    />
  )
}

Reference

ItemWrapper

Prop

Type

ItemGroupWrapper

Prop

Type