Oblivion
Oblivion
Docs
Go back
Component by
OV
Oblivion
hover me
TSX
glass-tooltip
"use client"

import React, { useState, useRef } from "react"
import { motion, AnimatePresence } from "framer-motion"
import { cn } from "@/lib/utils"

export interface GlassTooltipProps {
  children?: React.ReactNode
  label?: React.ReactNode
  position?: "top" | "bottom" | "left" | "right"
  delay?: number
  className?: string
  contentClassName?: string
}

export const componentMeta = {
  name: "Glass Tooltip",
  description: "An aesthetic, physics-based glassmorphism tooltip with subtle borders and fluid transitions.",
  props: {
    label: { type: "ReactNode", description: "The content inside the tooltip", default: '"Majestic Tooltip"' },
    position: { type: "enum", description: "Where the tooltip appears", default: '"top"' },
    delay: { type: "number", description: "Show delay in ms", default: "0" },
    children: { type: "ReactNode", description: "The trigger element" }
  }
}

// ─── Internal Pure Component ──────────────────────────────────────────────

function TooltipCore({ children, label, position = "top", delay = 0, contentClassName }: GlassTooltipProps) {
  const [isVisible, setIsVisible] = useState(false)
  const timerRef = useRef<NodeJS.Timeout | null>(null)

  const handleMouseEnter = () => {
    timerRef.current = setTimeout(() => setIsVisible(true), delay)
  }

  const handleMouseLeave = () => {
    if (timerRef.current) clearTimeout(timerRef.current)
    setIsVisible(false)
  }

  const variants = {
    initial: {
      opacity: 0,
      scale: 0.92,
      y: position === "top" ? 8 : position === "bottom" ? -8 : 0,
      x: position === "left" ? 8 : position === "right" ? -8 : 0,
    },
    animate: {
      opacity: 1,
      scale: 1,
      y: 0,
      x: 0,
      transition: { type: "spring" as const, stiffness: 450, damping: 25 }
    },
    exit: {
      opacity: 0,
      scale: 0.95,
      y: position === "top" ? 4 : position === "bottom" ? -4 : 0,
      x: position === "left" ? 4 : position === "right" ? -4 : 0,
      transition: { duration: 0.1 }
    }
  }

  const posClasses = {
    top: "bottom-full left-1/2 -translate-x-1/2 mb-3",
    bottom: "top-full left-1/2 -translate-x-1/2 mt-3",
    left: "right-full top-1/2 -translate-y-1/2 mr-3",
    right: "left-full top-1/2 -translate-y-1/2 ml-3"
  }

  const arrowClasses = {
    top: "top-full left-1/2 -translate-x-1/2 border-t-white/80 dark:border-t-zinc-800 border-x-transparent border-b-transparent",
    bottom: "bottom-full left-1/2 -translate-x-1/2 border-b-white/80 dark:border-b-zinc-800 border-x-transparent border-t-transparent",
    left: "left-full top-1/2 -translate-y-1/2 border-l-white/80 dark:border-l-zinc-800 border-y-transparent border-r-transparent",
    right: "right-full top-1/2 -translate-y-1/2 border-r-white/80 dark:border-r-zinc-800 border-y-transparent border-l-transparent"
  }

  return (
    <div 
      className="relative inline-block"
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
    >
      {children}
      <AnimatePresence>
        {isVisible && (
          <motion.div
            initial="initial" animate="animate" exit="exit" variants={variants}
            className={cn("absolute z-50 pointer-events-none select-none", posClasses[position as keyof typeof posClasses])}
          >
            <div className={cn(
              "relative px-3 py-1.5 rounded-xl text-xs font-medium tracking-tight text-neutral-900 dark:text-neutral-100 shadow-2xl overflow-hidden min-w-[max-content]",
              "bg-white/80 dark:bg-zinc-800/80 backdrop-blur-xl border border-white/20 dark:border-white/10",
              contentClassName
            )}>
              <div className="relative z-10 antialiased flex items-center gap-2">
                 {label || "Tooltip"}
              </div>
            </div>
            <div className={cn("absolute w-0 h-0 border-4", arrowClasses[position as keyof typeof arrowClasses])} />
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  )
}

// ─── Main Export ──────────────────────────────────────────────────────────

export default function GlassTooltip(props: GlassTooltipProps) {
  if (!props.children) {
    return (
      <div className="flex flex-col items-center justify-center min-h-[500px] w-full bg-slate-50 dark:bg-[#000] rounded-[2rem] p-12 overflow-hidden relative border border-black/5 dark:border-white/5 font-sans group/stage">
        <TooltipCore position="top" label={
          <div className="font-medium px-2 py-0.5 max-w-[200px] leading-snug">
            "The first rule of Fight Club is: you do not talk about Fight Club."
          </div>
        }>
          <motion.div 
            whileHover={{ scale: 1.02 }}
            whileTap={{ scale: 0.98 }}
            className="px-10 py-5 rounded-full bg-white dark:bg-zinc-900 text-neutral-800 dark:text-neutral-200 font-bold text-lg cursor-default border border-black/5 dark:border-white/10 shadow-xl transition-all hover:bg-neutral-50 dark:hover:bg-zinc-800"
          >
            hover me
          </motion.div>
        </TooltipCore>
      </div>
    )
  }

  return <TooltipCore {...props} />
}
Save to favoritesCopy to Figma
React

Glass Tooltip

An aesthetic, physics-based glassmorphism tooltip with subtle borders and fluid transitions.

Installation

$ npx @cosmoo/oblivion add glass-tooltip
Dependenciesnpm install framer-motion

Usage Example

tsx
import GlassTooltip from "@/components/oblivion/glass-tooltip"

export default function Demo() {
  return (
    <div className="p-10 flex justify-center">
      <GlassTooltip />
    </div>
  )
}