Try hovering below
DocsPricing
glide-tooltip.tsx
"use client";
import { motion, useReducedMotion } from "framer-motion";
import { List, MessageCircle, Upload, type LucideIcon } from "lucide-react";
import {
createContext,
useCallback,
useContext,
useRef,
useState,
type ReactNode,
} from "react";
import { cn } from "@/lib/utils";
interface TipState {
label: string;
icon?: LucideIcon;
kbd?: string[];
center: number;
}
const GlideCtx = createContext<{
show: (
label: string,
icon: LucideIcon | undefined,
kbd: string[] | undefined,
el: HTMLElement,
) => void;
hide: () => void;
} | null>(null);
export function GlideTooltipGroup({
orientation = "vertical",
preview,
className,
tooltipClassName,
children,
}: {
orientation?: "vertical" | "horizontal";
preview?: TipState;
className?: string;
tooltipClassName?: string;
children: ReactNode;
}) {
const reduce = useReducedMotion();
const groupRef = useRef<HTMLDivElement>(null);
const [tip, setTip] = useState<TipState | null>(preview ?? null);
const locked = preview !== undefined;
const horizontal = orientation === "horizontal";
const show = useCallback(
(
label: string,
icon: LucideIcon | undefined,
kbd: string[] | undefined,
el: HTMLElement,
) => {
const group = groupRef.current;
if (!group) return;
const g = group.getBoundingClientRect();
const r = el.getBoundingClientRect();
setTip({
label,
icon,
kbd,
center: horizontal
? r.left - g.left + r.width / 2
: r.top - g.top + r.height / 2,
});
},
[horizontal],
);
const hide = useCallback(() => setTip(null), []);
return (
<GlideCtx.Provider
value={locked ? { show: () => {}, hide: () => {} } : { show, hide }}
>
<div
ref={groupRef}
onMouseLeave={locked ? undefined : hide}
onBlur={
locked
? undefined
: (e) => {
if (!e.currentTarget.contains(e.relatedTarget as Node | null))
hide();
}
}
className={cn("relative", className)}
>
{children}
<motion.div
aria-hidden
className={cn(
"pointer-events-none absolute z-10",
horizontal ? "bottom-full left-0 mb-3" : "right-full top-0 mr-3",
)}
initial={false}
animate={
horizontal
? {
left: tip?.center ?? 0,
opacity: tip === null ? 0 : 1,
scale: tip === null ? 0.92 : 1,
}
: {
top: tip?.center ?? 0,
opacity: tip === null ? 0 : 1,
scale: tip === null ? 0.92 : 1,
}
}
transition={
reduce
? { duration: 0 }
: { type: "spring", stiffness: 550, damping: 38, mass: 0.6 }
}
style={{ transformOrigin: horizontal ? "50% 100%" : "100% 50%" }}
>
<div
className={cn(
"flex items-center gap-2 whitespace-nowrap rounded-[14px] bg-black px-4 py-2.5 text-sm font-medium text-ink shadow-2xl shadow-black/60",
horizontal ? "-translate-x-1/2" : "-translate-y-1/2",
tooltipClassName,
)}
>
{tip?.icon ? (
<tip.icon className="h-5 w-5 shrink-0 text-ink" />
) : null}
{tip?.label}
{tip?.kbd?.map((k) => (
<kbd
key={k}
className="rounded-md bg-white/10 px-1.5 py-0.5 font-mono text-[11px] leading-none text-ink/70"
>
{k}
</kbd>
))}
</div>
</motion.div>
</div>
</GlideCtx.Provider>
);
}
export function GlideTooltip({
label,
icon,
kbd,
className,
children,
}: {
label: string;
icon?: LucideIcon;
kbd?: string[];
className?: string;
children: ReactNode;
}) {
const ctx = useContext(GlideCtx);
const ref = useRef<HTMLSpanElement>(null);
const show = () => {
if (ref.current) ctx?.show(label, icon, kbd, ref.current);
};
return (
<span
ref={ref}
onMouseEnter={show}
onFocus={show}
className={cn("inline-flex", className)}
>
{children}
</span>
);
}
export interface TooltipDockItem {
icon: LucideIcon;
label: string;
kbd?: string[];
dot?: boolean;
active?: boolean;
}
export const TOOLTIP_DOCK_DEMO_ITEMS: TooltipDockItem[] = [
{ icon: MessageCircle, label: "Comment", kbd: ["C"], active: true },
{ icon: Upload, label: "Share" },
{ icon: List, label: "Menu", kbd: ["⌘", "K"], dot: true },
];
const ITEM = 44;
const GAP = 4;
const PAD = 8;
const centerOf = (i: number) => PAD + i * (ITEM + GAP) + ITEM / 2;
export function TooltipDock({
items = TOOLTIP_DOCK_DEMO_ITEMS,
orientation = "vertical",
previewIndex,
className,
}: {
items?: TooltipDockItem[];
orientation?: "vertical" | "horizontal";
previewIndex?: number;
className?: string;
}) {
const horizontal = orientation === "horizontal";
const previewItem =
previewIndex !== undefined ? items[previewIndex] : undefined;
return (
<GlideTooltipGroup
orientation={orientation}
preview={
previewItem
? {
label: previewItem.label,
icon: previewItem.icon,
kbd: previewItem.kbd,
center: centerOf(previewIndex ?? 0),
}
: undefined
}
className={cn(
"flex gap-1 rounded-[26px] bg-black p-2 shadow-2xl shadow-black/60",
horizontal ? "flex-row" : "flex-col",
className,
)}
>
{items.map((item) => {
const Icon = item.icon;
return (
<GlideTooltip
key={item.label}
label={item.label}
icon={item.icon}
kbd={item.kbd}
>
<button
type="button"
aria-label={
item.kbd ? item.label + ", " + item.kbd.join(" ") : item.label
}
className={cn(
"relative grid h-11 w-11 place-items-center rounded-full transition-colors duration-200",
item.active
? "bg-white/10 text-ink"
: "text-ink/55 hover:bg-white/[0.06] hover:text-ink",
)}
>
<Icon className="h-5 w-5" />
{item.dot && (
<span
aria-hidden
className="absolute right-2.5 top-2.5 h-1.5 w-1.5 rounded-full bg-blue-500"
/>
)}
</button>
</GlideTooltip>
);
})}
</GlideTooltipGroup>
);
}