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=dropdown-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