Glrk UI

Hover Card

A wrapper for Base UI Preview Card with a convenience API for title, description, hover delay, and arrows.

Trigger behaviour
Basic — opens on hover with default delay@glrk_ui
Delay — 800 ms open delay, 500 ms close grace periodHover (delayed)
Custom content — arbitrary node instead of title/descriptionView profile
Controlled
External state — button outside HoverCardWrapper drives open/close
Anchor
Arrow
showArrow — directional arrow points back at the trigger
Positioning
side — top / right / bottom / left

Installation

npx shadcn@latest add @glrk-ui/hover-card

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

Copy and paste the following code into your project.

ui/hover-card.tsx
'use client'

import { PreviewCard as HoverCardPrimitive } from '@base-ui/react/preview-card'
import type * as React from 'react'

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

function HoverCard(props: HoverCardPrimitive.Root.Props) {
  return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
}

function HoverCardTrigger({ className, ...props }: HoverCardPrimitive.Trigger.Props) {
  return (
    <HoverCardPrimitive.Trigger
      data-slot="hover-card-trigger"
      className={cn(className)}
      {...props}
    />
  )
}

function HoverCardArrow({ className, ...props }: HoverCardPrimitive.Arrow.Props) {
  return (
    <HoverCardPrimitive.Arrow
      data-slot="hover-card-arrow"
      className={cn(
        'flex data-[side=bottom]:top-[-10px] data-[side=left]:right-[-14px] data-[side=left]:rotate-90 data-[side=right]:left-[-14px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-10px] data-[side=top]:rotate-180',
        className,
      )}
      {...props}
    >
      <svg width="20" height="10" viewBox="0 0 20 10" className="block">
        <path d="M 0 10 L 10 0 L 20 10 Z" className="fill-popover" />
        <path
          d="M 0 10 L 10 0 L 20 10"
          className="fill-none stroke-foreground/10"
          strokeWidth="1"
          strokeLinejoin="round"
        />
      </svg>
    </HoverCardPrimitive.Arrow>
  )
}

type hoverCardContentType = HoverCardPrimitive.Popup.Props &
  Pick<
    HoverCardPrimitive.Positioner.Props,
    | 'align'
    | 'alignOffset'
    | 'side'
    | 'sideOffset'
    | 'arrowPadding'
    | 'collisionAvoidance'
    | 'collisionBoundary'
    | 'collisionPadding'
  > & {
    showArrow?: boolean
    arrowClassName?: string
  }

function HoverCardContent({
  className,
  align = 'center',
  alignOffset = 0,
  side = 'bottom',
  sideOffset = 4,
  arrowPadding,
  collisionAvoidance,
  collisionBoundary,
  collisionPadding,
  showArrow,
  arrowClassName,
  children,
  ...props
}: hoverCardContentType) {
  return (
    <HoverCardPrimitive.Portal>
      <HoverCardPrimitive.Positioner
        side={side}
        align={align}
        sideOffset={sideOffset}
        alignOffset={alignOffset}
        arrowPadding={arrowPadding}
        collisionPadding={collisionPadding}
        collisionBoundary={collisionBoundary}
        collisionAvoidance={collisionAvoidance}
      >
        <HoverCardPrimitive.Popup
          data-slot="hover-card-content"
          className={cn(
            'flex w-72 origin-(--transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
            className,
          )}
          {...props}
        >
          {children}
          {showArrow && <HoverCardArrow className={arrowClassName} />}
        </HoverCardPrimitive.Popup>
      </HoverCardPrimitive.Positioner>
    </HoverCardPrimitive.Portal>
  )
}

function HoverCardHeader({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="hover-card-header"
      className={cn('flex flex-col gap-0.5 text-sm', className)}
      {...props}
    />
  )
}

function HoverCardTitle({ className, ...props }: React.ComponentProps<'div'>) {
  return <div data-slot="hover-card-title" className={cn('font-medium', className)} {...props} />
}

function HoverCardDescription({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="hover-card-description"
      className={cn('text-muted-foreground', className)}
      {...props}
    />
  )
}

type HoverCardWrapperProps = {
  trigger?: React.ReactNode
  content?: React.ReactNode
  title?: React.ReactNode
  description?: React.ReactNode
  triggerCls?: string
  triggerProps?: Omit<HoverCardPrimitive.Trigger.Props, 'className' | 'children'>
  contentCls?: string
  contentProps?: Omit<hoverCardContentType, 'className'>
} & Omit<React.ComponentProps<typeof HoverCardPrimitive.Root>, 'children'>

function HoverCardWrapper({
  trigger,
  content,
  title,
  description,
  triggerCls,
  triggerProps,
  contentCls,
  contentProps,
  ...props
}: HoverCardWrapperProps) {
  return (
    <HoverCard {...props}>
      <HoverCardTrigger className={cn(triggerCls)} {...triggerProps}>
        {trigger}
      </HoverCardTrigger>

      <HoverCardContent {...contentProps} className={cn(contentCls)}>
        {(title || description) && (
          <HoverCardHeader>
            {title && <HoverCardTitle>{title}</HoverCardTitle>}
            {description && <HoverCardDescription>{description}</HoverCardDescription>}
          </HoverCardHeader>
        )}
        {content}
      </HoverCardContent>
    </HoverCard>
  )
}

export {
  HoverCard,
  HoverCardArrow,
  HoverCardContent,
  HoverCardDescription,
  HoverCardHeader,
  HoverCardTitle,
  HoverCardTrigger,
  HoverCardWrapper,
}

Usage

Basic

import { HoverCardWrapper } from "@/components/ui/hover-card"

<HoverCardWrapper
  trigger="@glrk_ui"
  triggerCls="px-4 py-1.5 border rounded text-sm"
  title="Glrk UI"
  description="Supporting description text."
/>

Title and description

Pass title and description directly — renders as HoverCardTitle and HoverCardDescription inside a HoverCardHeader:

<HoverCardWrapper
  trigger="Hover me"
  title="Card title"
  description="Supporting description text."
/>

For arbitrary content, use content instead — useful for things like a profile preview:

<HoverCardWrapper
  trigger="View profile"
  content={
    <div className="flex items-center gap-3">
      <div className="size-9 rounded-full bg-foreground" />
      <div className="flex flex-col gap-0.5">
        <span className="text-sm font-medium">Alice Chen</span>
        <span className="text-xs text-muted-foreground">Product Designer</span>
      </div>
    </div>
  }
/>

Delay

Opens on hover after delay ms, closes after closeDelay ms once the pointer leaves:

<HoverCardWrapper
  trigger="Hover (delayed)"
  triggerProps={{ delay: 800, closeDelay: 500 }}
  title="Delayed card"
  description="800 ms open delay, 500 ms close grace."
/>

Controlled

const [open, setOpen] = React.useState(false)

<HoverCardWrapper
  open={open}
  onOpenChange={setOpen}
  trigger="Anchor"
  title="Controlled"
  description="Driven by external state."
/>

Arrow

<HoverCardWrapper
  trigger="Hover"
  contentProps={{ showArrow: true, sideOffset: 10 }}
  title="With arrow"
  description="Arrow points toward the trigger."
/>

Control placement with side:

<HoverCardWrapper
  trigger="Top"
  contentProps={{ side: "top", showArrow: true, sideOffset: 10 }}
  title="side: top"
  description="Arrow points toward trigger."
/>

Positioning

<HoverCardWrapper trigger="Top"    contentProps={{ side: "top" }}    title="Top" />
<HoverCardWrapper trigger="Right"  contentProps={{ side: "right" }}  title="Right" />
<HoverCardWrapper trigger="Bottom" contentProps={{ side: "bottom" }} title="Bottom" />
<HoverCardWrapper trigger="Left"   contentProps={{ side: "left" }}   title="Left" />

Reference

HoverCardWrapper

Prop

Type