A cursor-following tooltip that squishes and skews based on mouse velocity.
Made by LéoTitle only
Sweep your cursor quickly
"use client";
import { FloatingTooltip } from "@/components/unlumen-ui/floating-tooltip";
import {
Share2,
Pencil,
Copy,
Archive,
Trash2,
MoveHorizontal,
MoveVertical,
} from "lucide-react";
const ACTIONS = [
{
label: "Share",
Icon: Share2,
content: "Share link",
description: "Copy a shareable link to clipboard.",
},
{
label: "Edit",
Icon: Pencil,
content: "Edit",
description: "Open in the inline editor.",
},
{
label: "Duplicate",
Icon: Copy,
content: "Duplicate",
description: "Creates an exact copy in this folder.",
},
{
label: "Archive",
Icon: Archive,
content: "Archive",
description: "Move to archive — reversible at any time.",
},
{
label: "Delete",
Icon: Trash2,
content: "Delete permanently",
description: "This action cannot be undone.",
},
];
export function FloatingTooltipDemo() {
return (
<FloatingTooltip.Provider>
<div className="flex flex-col gap-10 p-10 max-w-lg mx-auto w-full select-none">
{/* Section 1 — title only */}
<div className="flex flex-col gap-3">
<p className="text-sm font-medium text-muted-foreground">
Title only
</p>
<div className="flex flex-wrap gap-2">
{ACTIONS.map(({ label, Icon, content }) => (
<FloatingTooltip.Trigger key={label} content={content}>
<button className="flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent">
<Icon className="size-4" />
<span>{label}</span>
</button>
</FloatingTooltip.Trigger>
))}
</div>
</div>
{/* Section 3 — velocity squish */}
<div className="flex flex-col justify-center gap-3">
<FloatingTooltip.Trigger
content="Velocity-driven shape"
description="Squish, skew & border-radius all react to cursor speed in real time."
>
<div className="flex flex-col h-72 w-full items-center text-center justify-center gap-3 rounded-xl border border-dashed border-border bg-accent/30 text-sm text-muted-foreground transition-colors hover:bg-accent/50">
<MoveVertical className="size-4" />
<span>Sweep your cursor quickly</span>
<MoveVertical className="size-4" />
</div>
</FloatingTooltip.Trigger>
</div>
</div>
</FloatingTooltip.Provider>
);
}Installation
Install the following dependencies:
Copy and paste the following code into your project:
"use client";
import { createContext, useContext, useEffect, useState } from "react";
import { createPortal } from "react-dom";
import {
AnimatePresence,
motion,
useMotionValue,
useSpring,
useVelocity,
useTransform,
} from "motion/react";
import { cn } from "@/lib/utils";
interface TooltipContextType {
setContent: (content: string, description?: string) => void;
setIsActive: (active: boolean) => void;
}
const TooltipContext = createContext<TooltipContextType | null>(null);
export function FloatingTooltipProvider({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
const x = useMotionValue(0);
const y = useMotionValue(0);
const springConfig = { damping: 25, stiffness: 300 };
const smoothX = useSpring(x, springConfig);
const smoothY = useSpring(y, springConfig);
const velocityX = useVelocity(smoothX);
const velocityY = useVelocity(smoothY);
const scaleX = useTransform(velocityX, [-1000, 0, 1000], [0.9, 1, 1.15]);
const scaleY = useTransform(velocityY, [-1000, 0, 1000], [1.15, 1, 0.9]);
const skewX = useTransform(velocityX, [-1000, 0, 1000], [-3, 0, 3]);
const skewY = useTransform(velocityY, [-1000, 0, 1000], [-3, 0, 3]);
const borderRadius = useTransform([velocityX, velocityY], ([vx, vy]) => {
const velocity = Math.sqrt((vx as number) ** 2 + (vy as number) ** 2);
const radius = 8 + Math.min(velocity / 80, 16);
return `${radius}px`;
});
const [isActive, setIsActive] = useState(false);
const [content, setContent] = useState("");
const [description, setDescription] = useState("");
useEffect(() => {
if (typeof window === "undefined") return;
const getZoom = () => {
const htmlElement = document.documentElement;
const computedZoom = window.getComputedStyle(htmlElement).zoom;
return computedZoom ? parseFloat(computedZoom) : 1;
};
const handleMouseMove = (e: MouseEvent) => {
const zoom = getZoom();
x.set(e.clientX / zoom);
y.set(e.clientY / zoom);
};
window.addEventListener("mousemove", handleMouseMove, { passive: true });
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, [x, y]);
const handleSetContent = (newContent: string, newDescription?: string) => {
setContent(newContent);
setDescription(newDescription || "");
};
return (
<TooltipContext.Provider
value={{ setContent: handleSetContent, setIsActive }}
>
{children}
{typeof document !== "undefined" &&
createPortal(
<AnimatePresence>
{isActive && content && (
<motion.div
className="pointer-events-none fixed z-50"
style={{
top: smoothY,
left: smoothX,
}}
initial={{
opacity: 0,
scale: 0.8,
}}
animate={{
opacity: 1,
scale: 1,
}}
exit={{
opacity: 0,
scale: 0.8,
}}
transition={{
duration: 0.15,
ease: "easeOut",
}}
>
<motion.div
layout
className={cn(
"ml-4 mt-4 bg-primary dark:bg-white px-4 py-3 text-sm font-medium text-background shadow-lg",
className,
)}
style={{
scaleX,
scaleY,
skewX,
skewY,
borderRadius,
}}
transition={{
layout: {
type: "spring",
damping: 25,
stiffness: 400,
},
}}
>
<motion.div
key={content}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.15 }}
className="flex flex-col gap-1"
>
<span className="whitespace-nowrap font-semibold">
{content}
</span>
{description && (
<span className="max-w-[28ch] whitespace-normal text-sm leading-snug opacity-70 font-normal">
{description}
</span>
)}
</motion.div>
</motion.div>
</motion.div>
)}
</AnimatePresence>,
document.body,
)}
</TooltipContext.Provider>
);
}
export function FloatingTooltipTrigger({
children,
content,
description,
}: {
children: React.ReactNode;
content: string;
description?: string;
}) {
const context = useContext(TooltipContext);
if (!context) {
throw new Error(
"FloatingTooltipTrigger must be used within FloatingTooltipProvider",
);
}
const { setContent, setIsActive } = context;
const handleMouseEnter = () => {
setContent(content, description);
setIsActive(true);
};
const handleMouseLeave = () => {
setIsActive(false);
};
return (
<div onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
{children}
</div>
);
}
export const FloatingTooltip = {
Provider: FloatingTooltipProvider,
Trigger: FloatingTooltipTrigger,
};Update the import paths to match your project setup.
Usage
import { FloatingTooltip } from "@/components/unlumen-ui/floating-tooltip";
<FloatingTooltip.Provider>
<FloatingTooltip.Trigger content="Hello" description="Optional description">
<button>Hover me</button>
</FloatingTooltip.Trigger>
</FloatingTooltip.Provider>;API Reference
FloatingTooltip.Provider
Wraps your content and renders the tooltip portal. Place it high in your tree so all triggers share a single tooltip instance.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Content to render |
className | string | — | Extra classes applied to the tooltip bubble |
FloatingTooltip.Trigger
Attach this around any element to make it show the tooltip on hover.
| Prop | Type | Default | Description |
|---|---|---|---|
content | string | — | Main label shown in the tooltip |
description | string | — | Optional secondary text shown below the label |
children | ReactNode | — | The element that triggers the tooltip |