Glrk UI

Tooltip

A wrapper for Base UI Tooltip with delay control, arrow toggle, render prop trigger, controlled state, and Provider for shared timing.

Basic
Default — hover trigger, top placement, arrow shown
Delay
delay / closeDelay — per-tooltip timing
TooltipProvider — shared delay + instant-reopen window across multiple tooltips
Arrow
showArrow — toggle the directional arrow (default: true)
Render prop trigger
triggerProps.render — trigger style reacts to open state, no useState needed
Controlled
open + onOpenChange — driven by external state
Positioning
side — top / right / bottom / left

Installation

npx shadcn@latest add @glrk-ui/tooltip

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

Copy and paste the following code into your project.

ui/tooltip.tsx
'use client'

import * as React from 'react'
import { Tooltip as TooltipPrimitive } from '@base-ui/react/tooltip'

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

function TooltipProvider({ delay = 0, ...props }: TooltipPrimitive.Provider.Props) {
  return <TooltipPrimitive.Provider data-slot="tooltip-provider" delay={delay} {...props} />
}

function Tooltip(props: TooltipPrimitive.Root.Props) {
  return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
}

function TooltipTrigger(props: TooltipPrimitive.Trigger.Props) {
  return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}

function TooltipArrow({ className, ...props }: TooltipPrimitive.Arrow.Props) {
  return (
    <TooltipPrimitive.Arrow
      data-slot="tooltip-arrow"
      className={cn(
        'size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5',
        className,
      )}
      {...props}
    />
  )
}

type tooltipContentT = TooltipPrimitive.Popup.Props &
  Pick<TooltipPrimitive.Positioner.Props, 'align' | 'alignOffset' | 'side' | 'sideOffset'> & {
    showArrow?: boolean
    arrowClassName?: string
  }

function TooltipContent({
  className,
  side = 'top',
  sideOffset = 4,
  align = 'center',
  alignOffset = 0,
  showArrow = true,
  arrowClassName,
  children,
  ...props
}: tooltipContentT) {
  return (
    <TooltipPrimitive.Portal>
      <TooltipPrimitive.Positioner
        align={align}
        alignOffset={alignOffset}
        side={side}
        sideOffset={sideOffset}
      >
        <TooltipPrimitive.Popup
          data-slot="tooltip-content"
          className={cn(
            'inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 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-[slot=kbd]:relative **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 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 && <TooltipArrow className={arrowClassName} />}
        </TooltipPrimitive.Popup>
      </TooltipPrimitive.Positioner>
    </TooltipPrimitive.Portal>
  )
}

type TooltipWrapperProps = {
  trigger?: React.ReactNode
  content: React.ReactNode
  triggerCls?: string
  triggerProps?: Omit<TooltipPrimitive.Trigger.Props, 'className' | 'children'>
  contentCls?: string
  contentProps?: Omit<tooltipContentT, 'className'>
} & Omit<TooltipPrimitive.Root.Props, 'children'>

function TooltipWrapper({
  trigger,
  content,
  triggerCls,
  triggerProps,
  contentCls,
  contentProps,
  ...props
}: TooltipWrapperProps) {
  return (
    <Tooltip {...props}>
      <TooltipTrigger className={cn(triggerCls)} {...triggerProps}>
        {trigger}
      </TooltipTrigger>

      <TooltipContent {...contentProps} className={cn(contentCls)}>
        {content}
      </TooltipContent>
    </Tooltip>
  )
}

export { Tooltip, TooltipArrow, TooltipContent, TooltipProvider, TooltipTrigger, TooltipWrapper }

Usage

Basic

import { TooltipWrapper } from "@/components/ui/tooltip"

<TooltipWrapper
  trigger="Hover me"
  triggerCls="px-4 py-1.5 border rounded text-sm"
  content="Tooltip text"
/>

Delay

delay and closeDelay are Trigger-level props — pass them via triggerProps:

<TooltipWrapper trigger="Instant"     content="No delay"           triggerProps={{ delay: 0 }} />
<TooltipWrapper trigger="600 ms"      content="Opens after 600 ms" triggerProps={{ delay: 600 }} />
<TooltipWrapper trigger="Close delay" content="500 ms close grace" triggerProps={{ closeDelay: 500 }} />

Provider

Wrap multiple tooltips in TooltipProvider to share delay and enable the instant-reopen window (timeout). If the user moves between tooltips within the timeout window, the next one opens instantly regardless of delay:

import { TooltipProvider, TooltipWrapper } from "@/components/ui/tooltip"

<TooltipProvider delay={600} closeDelay={200}>
  <TooltipWrapper trigger="First"  content="First tooltip" />
  <TooltipWrapper trigger="Second" content="Second tooltip" />
  <TooltipWrapper trigger="Third"  content="Third tooltip" />
</TooltipProvider>

Arrow

Arrow is shown by default. Hide it via contentProps:

<TooltipWrapper trigger="No arrow" content="Arrow hidden" contentProps={{ showArrow: false }} />

Render prop trigger

Use triggerProps.render for state-aware trigger styling. trigger can be omitted when the render prop is provided:

<TooltipWrapper
  triggerProps={{
    render: (props, state) => (
      <button
        {...props}
        className={state.open ? "bg-foreground text-background rounded px-4 py-1.5 text-sm" : "border rounded px-4 py-1.5 text-sm"}
      >
        {state.open ? "Showing ✓" : "Hover"}
      </button>
    ),
  }}
  content="Trigger style reacts to open state."
/>

Controlled

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

<TooltipWrapper
  open={open}
  onOpenChange={setOpen}
  trigger="Target"
  content="Controlled tooltip"
/>

Positioning

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

Using primitives

For render prop triggers or custom layouts, use primitives directly:

import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"

<Tooltip>
  <TooltipTrigger
    render={(props, state) => (
      <button {...props} className={state.open ? "bg-foreground text-background" : "border"}>
        Hover
      </button>
    )}
  />
  <TooltipContent side="right" sideOffset={8}>
    Built with primitives
  </TooltipContent>
</Tooltip>

Reference

TooltipWrapper

Prop

Type

TooltipProvider

Prop

Type