New: Activity Terrain +12 more

Item

A row of media, text and actions for lists, settings and pickers, with a grouped surface and a hover highlight that glides between rows.

13 examplesDepends on@base-ui/react@tabler/icons-reactclass-variance-authoritycnFrom HextaUI (MIT)

Installation

Run this in your project. It adds the files, installs the npm packages and anything this builds on, and sets up the theme:

npx shadcn@latest add https://kryptonite-ui.vercel.app/r/item.json
Install manually
  1. Install the dependencies:

    npm install @base-ui/react @tabler/icons-react class-variance-authority cn
  2. It builds on these, so copy them first:

  3. Copy this into components/ui/item.tsx:

    item.tsx
    "use client"
    
    import * as React from "react"
    import { mergeProps } from "@base-ui/react/merge-props"
    import { useRender } from "@base-ui/react/use-render"
    import { IconChevronRight } from "@tabler/icons-react"
    import { cva, type VariantProps } from "class-variance-authority"
    import { cn } from "cn"
    
    import { prefersReducedMotion } from "@/lib/motion"
    
    function dataAttributes(slot: string, extra?: Record<string, string>) {
      return { "data-slot": slot, ...extra } as Record<string, string>
    }
    
    const interactiveSelector =
      'a[href], button, label, summary, [role="button"], [role="link"], [role="option"], [role="radio"], [role="checkbox"], [role="menuitem"]'
    
    function isDisabled(element: Element) {
      return (
        element.matches(":disabled") ||
        element.getAttribute("aria-disabled") === "true"
      )
    }
    
    function markInteractive(element: HTMLElement | null) {
      if (!element) {
        return
      }
      if (element.matches(interactiveSelector)) {
        element.setAttribute("data-interactive", "")
      } else {
        element.removeAttribute("data-interactive")
      }
    }
    
    type ItemGroupVariant = "default" | "grouped"
    
    const itemGroupVariants = cva(
      "group/item-group relative isolate flex w-full min-w-0 flex-col",
      {
        variants: {
          variant: {
            default:
              "gap-4 has-[>[data-size=sm]]:gap-2.5 has-[>[data-size=xs]]:gap-2",
            grouped:
              "rounded-(--item-group-radius) bg-card text-card-foreground ring-(length:--hairline) ring-border [--item-group-radius:var(--radius-xl)] forced-colors:border [&>[data-slot=item]]:rounded-none [&>[data-slot=item]:not(:has(~[data-slot=item]))]:rounded-b-[calc(var(--item-group-radius)-var(--hairline))] [&>[data-slot=item]:not([data-slot=item]~[data-slot=item])]:rounded-t-[calc(var(--item-group-radius)-var(--hairline))]",
          },
        },
        defaultVariants: {
          variant: "default",
        },
      }
    )
    
    type ItemGroupProps = useRender.ComponentProps<"div"> & {
      variant?: ItemGroupVariant
      highlight?: boolean
      arrowNavigation?: boolean
    }
    
    function useHighlight(enabled: boolean) {
      const groupRef = React.useRef<HTMLElement | null>(null)
      const highlightRef = React.useRef<HTMLDivElement | null>(null)
      const currentRef = React.useRef<HTMLElement | null>(null)
      const hiddenAtRef = React.useRef(-Infinity)
    
      const place = React.useCallback((item: HTMLElement, instant: boolean) => {
        const highlight = highlightRef.current
        if (!highlight) {
          return
        }
        if (instant || prefersReducedMotion()) {
          highlight.setAttribute("data-instant", "")
        } else {
          highlight.removeAttribute("data-instant")
        }
        const group = item.parentElement
        const box = item.getBoundingClientRect()
        const frame = group?.getBoundingClientRect()
        const scale =
          group && frame && group.offsetWidth > 0
            ? frame.width / group.offsetWidth
            : 1
        const x =
          frame && group
            ? (box.left - frame.left) / scale - group.clientLeft
            : item.offsetLeft
        const y =
          frame && group
            ? (box.top - frame.top) / scale - group.clientTop
            : item.offsetTop
        highlight.style.left = "0px"
        highlight.style.width = `${box.width / scale}px`
        highlight.style.height = `${box.height / scale}px`
        highlight.style.transform = `translate(${x}px, ${y}px)`
        highlight.style.borderRadius = getComputedStyle(item).borderRadius
      }, [])
    
      const hide = React.useCallback(() => {
        if (highlightRef.current?.hasAttribute("data-visible")) {
          hiddenAtRef.current = performance.now()
        }
        currentRef.current?.removeAttribute("data-highlighted")
        currentRef.current = null
        highlightRef.current?.removeAttribute("data-visible")
        highlightRef.current?.removeAttribute("data-pressed")
      }, [])
    
      const show = React.useCallback(
        (item: HTMLElement) => {
          const highlight = highlightRef.current
          if (!highlight || currentRef.current === item) {
            return
          }
          const appearing =
            !highlight.hasAttribute("data-visible") &&
            performance.now() - hiddenAtRef.current > 200
          currentRef.current?.removeAttribute("data-highlighted")
          currentRef.current = item
          item.setAttribute("data-highlighted", "")
          place(item, appearing)
          if (appearing) {
            void highlight.offsetWidth
          }
          highlight.setAttribute("data-visible", "")
        },
        [place]
      )
    
      React.useEffect(() => {
        const group = groupRef.current
        if (!enabled || !group) {
          return
        }
    
        const itemFrom = (target: EventTarget | null) => {
          const item = (target as Element | null)?.closest<HTMLElement>(
            "[data-slot=item][data-interactive]"
          )
          if (
            !item ||
            item.parentElement !== group ||
            isDisabled(item) ||
            item.hasAttribute("data-disabled")
          ) {
            return null
          }
          return item
        }
    
        const onPointerOver = (event: PointerEvent) => {
          if (event.pointerType === "touch") {
            return
          }
          const item = itemFrom(event.target)
          if (item) {
            show(item)
          } else if (event.target !== group) {
            hide()
          }
        }
        const onPointerDown = (event: PointerEvent) => {
          if (event.pointerType !== "touch" && itemFrom(event.target)) {
            highlightRef.current?.setAttribute("data-pressed", "")
          }
        }
        const onPointerUp = () => {
          highlightRef.current?.removeAttribute("data-pressed")
        }
    
        const resize =
          typeof ResizeObserver === "undefined"
            ? null
            : new ResizeObserver(() => {
                if (currentRef.current?.isConnected) {
                  place(currentRef.current, true)
                } else if (currentRef.current) {
                  hide()
                }
              })
        resize?.observe(group)
    
        group.addEventListener("pointerover", onPointerOver)
        group.addEventListener("pointerleave", hide)
        group.addEventListener("pointerdown", onPointerDown)
        window.addEventListener("pointerup", onPointerUp)
        window.addEventListener("pointercancel", onPointerUp)
    
        return () => {
          resize?.disconnect()
          group.removeEventListener("pointerover", onPointerOver)
          group.removeEventListener("pointerleave", hide)
          group.removeEventListener("pointerdown", onPointerDown)
          window.removeEventListener("pointerup", onPointerUp)
          window.removeEventListener("pointercancel", onPointerUp)
          hide()
        }
      }, [enabled, show, hide, place])
    
      return { groupRef, highlightRef }
    }
    
    function useListRole(groupRef: React.RefObject<HTMLElement | null>) {
      const [list, setList] = React.useState(true)
    
      React.useEffect(() => {
        const group = groupRef.current
        if (!group) {
          return
        }
        const check = () => {
          const items = Array.from(group.children).filter(
            (child) => child.getAttribute("data-slot") === "item"
          )
          setList(
            items.length > 0 &&
              items.every((item) => item.getAttribute("role") === "listitem")
          )
        }
        check()
        const observer = new MutationObserver(check)
        observer.observe(group, { childList: true })
        return () => observer.disconnect()
      }, [groupRef])
    
      return list
    }
    
    function useSeparatorInset(
      groupRef: React.RefObject<HTMLElement | null>,
      enabled: boolean
    ) {
      React.useEffect(() => {
        const group = groupRef.current
        if (!enabled || !group) {
          return
        }
    
        let frame = 0
        const measure = () => {
          cancelAnimationFrame(frame)
          frame = requestAnimationFrame(() => {
            for (const item of Array.from(group.children)) {
              if (!(item instanceof HTMLElement) || item.dataset.slot !== "item") {
                continue
              }
              const content = item.querySelector<HTMLElement>(
                ":scope > [data-slot=item-content]"
              )
              if (!content) {
                item.style.removeProperty("--item-inset")
                continue
              }
              const box = item.getBoundingClientRect()
              const text = content.getBoundingClientRect()
              const scale = item.offsetWidth > 0 ? box.width / item.offsetWidth : 1
              const rtl = getComputedStyle(item).direction === "rtl"
              const inset = rtl ? box.right - text.right : text.left - box.left
              item.style.setProperty("--item-inset", `${inset / scale}px`)
            }
          })
        }
    
        measure()
        const resize =
          typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measure)
        resize?.observe(group)
        const mutations = new MutationObserver(measure)
        mutations.observe(group, { childList: true, subtree: true })
        document.fonts?.addEventListener?.("loadingdone", measure)
    
        return () => {
          cancelAnimationFrame(frame)
          resize?.disconnect()
          mutations.disconnect()
          document.fonts?.removeEventListener?.("loadingdone", measure)
          for (const item of Array.from(group.children)) {
            if (item instanceof HTMLElement) {
              item.style.removeProperty("--item-inset")
            }
          }
        }
      }, [groupRef, enabled])
    }
    
    const navigationKeys = new Set(["ArrowDown", "ArrowUp", "Home", "End"])
    
    function useArrowNavigation(
      groupRef: React.RefObject<HTMLElement | null>,
      enabled: boolean
    ) {
      React.useEffect(() => {
        const group = groupRef.current
        if (!enabled || !group) {
          return
        }
    
        const onKeyDown = (event: KeyboardEvent) => {
          const current = event.target as HTMLElement
          if (
            event.defaultPrevented ||
            !navigationKeys.has(event.key) ||
            event.altKey ||
            event.ctrlKey ||
            event.metaKey ||
            event.shiftKey ||
            current.parentElement !== group ||
            !current.matches("[data-slot=item][data-interactive]")
          ) {
            return
          }
    
          const items = Array.from(group.children).filter(
            (child): child is HTMLElement =>
              child instanceof HTMLElement &&
              child.matches("[data-slot=item][data-interactive]") &&
              !isDisabled(child) &&
              !child.hasAttribute("data-disabled") &&
              !child.hidden &&
              (typeof child.checkVisibility !== "function" ||
                child.checkVisibility())
          )
          const index = items.indexOf(current)
          const next =
            event.key === "Home"
              ? items[0]
              : event.key === "End"
                ? items.at(-1)
                : event.key === "ArrowDown"
                  ? items[index + 1]
                  : items[index - 1]
    
          event.preventDefault()
          if (next && next !== current) {
            next.focus()
            next.scrollIntoView?.({ block: "nearest" })
          }
        }
    
        group.addEventListener("keydown", onKeyDown)
        return () => group.removeEventListener("keydown", onKeyDown)
      }, [groupRef, enabled])
    }
    
    const ItemGroupContext = React.createContext<{
      variant: ItemGroupVariant
    } | null>(null)
    
    function ItemGroup({
      className,
      variant = "default",
      highlight = true,
      arrowNavigation = true,
      render,
      children,
      ...props
    }: ItemGroupProps) {
      const { groupRef, highlightRef } = useHighlight(highlight)
      useArrowNavigation(groupRef, arrowNavigation)
      useSeparatorInset(groupRef, variant === "grouped")
      const list = useListRole(groupRef)
      const context = React.useMemo(() => ({ variant }), [variant])
    
      const element = useRender({
        defaultTagName: "div",
        render,
        ref: groupRef,
        props: mergeProps<"div">(
          {
            role: list ? "list" : undefined,
            className: cn(itemGroupVariants({ variant }), className),
            children: (
              <>
                {highlight && (
                  <div
                    ref={highlightRef}
                    aria-hidden="true"
                    data-slot="item-highlight"
                    className="pointer-events-none absolute top-0 -z-1 bg-muted opacity-0 transition-[transform,width,height,opacity,border-radius,background-color] duration-200 ease-out-quint data-instant:transition-[opacity,background-color] data-pressed:bg-foreground/10 data-visible:opacity-100 motion-reduce:transition-opacity dark:bg-muted/70"
                  />
                )}
                {children}
              </>
            ),
          },
          props,
          dataAttributes("item-group", {
            "data-variant": variant,
            ...(highlight ? { "data-highlight": "" } : {}),
          })
        ),
      })
    
      return (
        <ItemGroupContext.Provider value={context}>
          {element}
        </ItemGroupContext.Provider>
      )
    }
    
    function ItemSeparator({
      className,
      render,
      ...props
    }: useRender.ComponentProps<"div">) {
      return useRender({
        defaultTagName: "div",
        render,
        props: mergeProps<"div">(
          {
            role: "none",
            "aria-hidden": true,
            className: cn(
              "h-(--hairline) w-full shrink-0 bg-border group-data-[variant=default]/item-group:my-2",
              className
            ),
          },
          props,
          dataAttributes("item-separator")
        ),
      })
    }
    
    const itemVariants = cva(
      "group/item relative flex w-full min-w-0 flex-wrap items-center gap-(--item-gap) rounded-(--item-radius) px-(--item-px) py-(--item-py) text-start text-sm text-foreground transition-[background-color,box-shadow,scale,translate,opacity] duration-200 ease-out-quint outline-none [--item-inset:var(--item-px)] before:pointer-events-none before:absolute focus-visible:outline-hidden focus-visible:before:opacity-0 has-data-checked:bg-muted/60 group-data-[variant=default]/item-group:has-data-checked:inset-ring-(length:--hairline) group-data-[variant=default]/item-group:has-data-checked:inset-ring-foreground/25 has-[>[data-slot=item-media][data-variant=icon]]:[--item-inset:calc(var(--item-px)+var(--item-media-size)+var(--item-gap))] has-[>[data-slot=item-media][data-variant=image]]:[--item-inset:calc(var(--item-px)+var(--item-media-size)+var(--item-gap))] aria-checked:bg-muted/60 group-data-[variant=default]/item-group:aria-checked:inset-ring-(length:--hairline) group-data-[variant=default]/item-group:aria-checked:inset-ring-foreground/25 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-pressed:bg-muted/60 group-data-[variant=default]/item-group:aria-pressed:inset-ring-(length:--hairline) group-data-[variant=default]/item-group:aria-pressed:inset-ring-foreground/25 aria-selected:bg-muted/60 group-data-[variant=default]/item-group:aria-selected:inset-ring-(length:--hairline) group-data-[variant=default]/item-group:aria-selected:inset-ring-foreground/25 data-highlighted:before:opacity-0 data-interactive:cursor-pointer data-interactive:select-none data-interactive:focus-visible:z-10 data-interactive:focus-visible:ring-3 data-interactive:focus-visible:inset-ring-(length:--hairline) data-interactive:focus-visible:ring-focus-ring data-interactive:focus-visible:inset-ring-ring group-data-[variant=grouped]/item-group:data-interactive:focus-visible:ring-0 group-data-[variant=grouped]/item-group:data-interactive:focus-visible:inset-ring-2 group-data-[variant=grouped]/item-group:data-interactive:focus-visible:inset-ring-ring/70 data-interactive:active:duration-100 not-group-data-[variant=grouped]/item-group:data-interactive:motion-safe:active:scale-[0.985] motion-reduce:transition-none data-disabled:pointer-events-none data-disabled:opacity-50 [&:disabled]:pointer-events-none [&:disabled]:opacity-50 [@media(hover:hover)]:not-group-data-highlight/item-group:data-interactive:hover:bg-muted/60 [[data-highlighted]+&]:before:opacity-0 [[data-slot=item-separator]+&]:before:hidden [[data-slot=item]:focus-visible+&]:before:opacity-0 group-data-[variant=grouped]/item-group:[[data-slot=item]~&]:before:start-(--item-inset) group-data-[variant=grouped]/item-group:[[data-slot=item]~&]:before:inset-e-0 group-data-[variant=grouped]/item-group:[[data-slot=item]~&]:before:top-0 group-data-[variant=grouped]/item-group:[[data-slot=item]~&]:before:h-(--hairline) group-data-[variant=grouped]/item-group:[[data-slot=item]~&]:before:bg-border group-data-[variant=grouped]/item-group:[[data-slot=item]~&]:before:transition-opacity group-data-[variant=grouped]/item-group:[[data-slot=item]~&]:before:duration-200 [[data-slot=separator]+&]:before:hidden",
      {
        variants: {
          variant: {
            default: "",
            outline:
              "inset-ring-(length:--hairline) inset-ring-border group-data-[variant=grouped]/item-group:inset-ring-0 forced-colors:border",
            muted:
              "bg-muted/50 group-data-[variant=grouped]/item-group:bg-transparent",
          },
          size: {
            default:
              "[--item-gap:--spacing(3.5)] [--item-media-size:--spacing(10)] [--item-px:--spacing(4)] [--item-py:--spacing(3.5)] [--item-radius:var(--radius-lg)]",
            sm: "[--item-gap:--spacing(2.5)] [--item-media-size:--spacing(8)] [--item-px:--spacing(3)] [--item-py:--spacing(2.5)] [--item-radius:var(--radius-md)]",
            xs: "[--item-gap:--spacing(2)] [--item-media-size:--spacing(6)] [--item-px:--spacing(2.5)] [--item-py:--spacing(2)] [--item-radius:var(--radius-md)]",
          },
        },
        defaultVariants: {
          variant: "default",
          size: "default",
        },
      }
    )
    
    type ItemVariant = NonNullable<VariantProps<typeof itemVariants>["variant"]>
    type ItemSize = NonNullable<VariantProps<typeof itemVariants>["size"]>
    
    type ItemProps = useRender.ComponentProps<"div"> & {
      variant?: ItemVariant
      size?: ItemSize
    }
    
    function Item({
      className,
      variant = "default",
      size = "default",
      render,
      ref,
      ...props
    }: ItemProps) {
      const group = React.useContext(ItemGroupContext)
      const setRef = React.useCallback(
        (node: HTMLElement | null) => {
          markInteractive(node)
          if (typeof ref === "function") {
            return ref(node as HTMLDivElement | null)
          }
          if (ref) {
            ref.current = node as HTMLDivElement | null
          }
          return undefined
        },
        [ref]
      )
    
      return useRender({
        defaultTagName: "div",
        render,
        ref: setRef,
        props: mergeProps<"div">(
          {
            role: group && !render ? "listitem" : undefined,
            className: cn(itemVariants({ variant, size }), className),
          },
          props,
          dataAttributes("item", { "data-variant": variant, "data-size": size })
        ),
      })
    }
    
    const itemMediaVariants = cva(
      "flex shrink-0 items-center justify-center gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
      {
        variants: {
          variant: {
            default: "bg-transparent",
            icon: "size-(--item-media-size) rounded-[max(calc(var(--radius-sm)*0.75),calc(var(--item-radius)-var(--item-py)))] bg-muted text-foreground ring-(length:--hairline) ring-foreground/5 group-data-[size=xs]/item:not-data-tone:bg-transparent group-data-[size=xs]/item:not-data-tone:ring-0 data-tone:bg-(--item-media-tone) data-tone:bg-linear-to-b data-tone:from-white/18 data-tone:to-transparent data-tone:text-white data-tone:shadow-[inset_0_var(--hairline)_0_rgb(255_255_255/0.25)] data-tone:ring-black/8 dark:data-tone:ring-white/10 forced-colors:border [&_svg:not([class*='size-'])]:size-4 group-data-[size=default]/item:[&_svg:not([class*='size-'])]:size-5",
            image:
              "size-(--item-media-size) overflow-hidden rounded-[max(calc(var(--radius-sm)*0.75),calc(var(--item-radius)-var(--item-py)))] bg-muted ring-(length:--hairline) ring-foreground/5 forced-colors:border [&_img]:size-full [&_img]:object-cover",
          },
          tone: {
            gray: "[--item-media-tone:var(--color-zinc-500)]",
            red: "[--item-media-tone:var(--color-red-500)]",
            orange: "[--item-media-tone:var(--color-orange-500)]",
            yellow: "[--item-media-tone:var(--color-amber-400)]",
            green: "[--item-media-tone:var(--color-green-500)]",
            teal: "[--item-media-tone:var(--color-teal-500)]",
            sky: "[--item-media-tone:var(--color-sky-500)]",
            blue: "[--item-media-tone:var(--color-blue-500)]",
            indigo: "[--item-media-tone:var(--color-indigo-500)]",
            purple: "[--item-media-tone:var(--color-purple-500)]",
            pink: "[--item-media-tone:var(--color-pink-500)]",
          },
        },
        defaultVariants: {
          variant: "default",
        },
      }
    )
    
    type ItemMediaTone = NonNullable<VariantProps<typeof itemMediaVariants>["tone"]>
    
    type ItemMediaVariant = NonNullable<
      VariantProps<typeof itemMediaVariants>["variant"]
    >
    
    type ItemMediaProps = useRender.ComponentProps<"div"> & {
      variant?: ItemMediaVariant
      tone?: ItemMediaTone
    }
    
    function ItemMedia({
      className,
      variant = "default",
      tone,
      render,
      ...props
    }: ItemMediaProps) {
      const tinted = variant === "icon" && tone ? tone : undefined
    
      return useRender({
        defaultTagName: "div",
        render,
        props: mergeProps<"div">(
          {
            className: cn(itemMediaVariants({ variant, tone: tinted }), className),
            ...(variant === "icon" ? { "aria-hidden": true } : {}),
          },
          props,
          dataAttributes("item-media", {
            "data-variant": variant,
            ...(tinted ? { "data-tone": tinted } : {}),
          })
        ),
      })
    }
    
    function ItemContent({
      className,
      render,
      ...props
    }: useRender.ComponentProps<"div">) {
      return useRender({
        defaultTagName: "div",
        render,
        props: mergeProps<"div">(
          {
            className: cn(
              "flex min-w-0 flex-1 flex-col gap-1 group-data-[size=sm]/item:gap-0 group-data-[size=xs]/item:gap-0",
              className
            ),
          },
          props,
          dataAttributes("item-content")
        ),
      })
    }
    
    function ItemTitle({
      className,
      render,
      ...props
    }: useRender.ComponentProps<"div">) {
      return useRender({
        defaultTagName: "div",
        render,
        props: mergeProps<"div">(
          {
            className: cn(
              "line-clamp-1 flex w-fit max-w-full min-w-0 items-center gap-2 text-sm leading-snug font-medium wrap-anywhere",
              className
            ),
          },
          props,
          dataAttributes("item-title")
        ),
      })
    }
    
    function ItemDescription({
      className,
      render,
      ...props
    }: useRender.ComponentProps<"p">) {
      return useRender({
        defaultTagName: "p",
        render,
        props: mergeProps<"p">(
          {
            className: cn(
              "line-clamp-2 min-w-0 text-start text-sm leading-normal font-normal text-pretty wrap-anywhere text-muted-foreground group-data-[size=xs]/item:text-xs [&_a:not([data-slot])]:text-foreground [&_a:not([data-slot])]:underline [&_a:not([data-slot])]:decoration-foreground/30 [&_a:not([data-slot])]:underline-offset-4 [&_a:not([data-slot])]:hover:decoration-foreground",
              className
            ),
          },
          props,
          dataAttributes("item-description") as React.ComponentProps<"p">
        ),
      })
    }
    
    function ItemActions({
      className,
      render,
      ...props
    }: useRender.ComponentProps<"div">) {
      return useRender({
        defaultTagName: "div",
        render,
        props: mergeProps<"div">(
          { className: cn("flex shrink-0 items-center gap-2", className) },
          props,
          dataAttributes("item-actions")
        ),
      })
    }
    
    function ItemHeader({
      className,
      render,
      ...props
    }: useRender.ComponentProps<"div">) {
      return useRender({
        defaultTagName: "div",
        render,
        props: mergeProps<"div">(
          {
            className: cn(
              "flex min-w-0 basis-full items-center justify-between gap-2",
              className
            ),
          },
          props,
          dataAttributes("item-header")
        ),
      })
    }
    
    function ItemFooter({
      className,
      render,
      ...props
    }: useRender.ComponentProps<"div">) {
      return useRender({
        defaultTagName: "div",
        render,
        props: mergeProps<"div">(
          {
            className: cn(
              "flex min-w-0 basis-full items-center justify-between gap-2",
              className
            ),
          },
          props,
          dataAttributes("item-footer")
        ),
      })
    }
    
    function ItemChevron({
      className,
      ...props
    }: React.ComponentProps<typeof IconChevronRight>) {
      return (
        <IconChevronRight
          aria-hidden="true"
          data-slot="item-chevron"
          className={cn(
            "size-4 shrink-0 text-muted-foreground transition-[translate,color] duration-200 ease-out-quint group-data-highlighted/item:text-foreground motion-safe:group-data-highlighted/item:translate-x-0.5 motion-reduce:transition-colors rtl:-scale-x-100 motion-safe:rtl:group-data-highlighted/item:-translate-x-0.5 [@media(hover:hover)]:group-data-interactive/item:group-hover/item:text-foreground motion-safe:[@media(hover:hover)]:group-data-interactive/item:group-hover/item:translate-x-0.5 motion-safe:[@media(hover:hover)]:rtl:group-data-interactive/item:group-hover/item:-translate-x-0.5",
            className
          )}
          {...props}
        />
      )
    }
    
    export {
      Item,
      ItemActions,
      ItemChevron,
      ItemContent,
      ItemDescription,
      ItemFooter,
      ItemGroup,
      ItemHeader,
      ItemMedia,
      ItemSeparator,
      ItemTitle,
      itemGroupVariants,
      itemMediaVariants,
      itemVariants,
    }
    export type { ItemGroupProps, ItemMediaProps, ItemMediaTone, ItemProps }

Examples

Actions

Avatar

Hover highlight

Long content

People

Grouped

Right to left

Selectable

Separator

Sizes

Variants

Feedback

Questions, bugs or ideas for Item? Post them here, or reply to someone else. Sign in with GitHub to comment.