Animated Tooltip
zyeon/animated-tooltipFreeComponent
A self-contained hover/focus tooltip with a spring pop-in, reduced-motion fallback and an arrow callout — no Radix dependency.
Install it
npx shadcn@latest add https://ui.zyeon.ai/r/animated-tooltip.jsonSource
1"use client"23import * as React from "react"4import { AnimatePresence, motion, useReducedMotion } from "motion/react"5import { cn } from "@/lib/utils"67export interface AnimatedTooltipProps {8 /** Tooltip bubble content. */9 content: React.ReactNode10 /** Which side of the trigger the bubble appears on. */11 side?: "top" | "bottom"12 /** The trigger — a single focusable element (button, link, or a tabIndex'd13 * element). The tooltip's id is injected into it via aria-describedby so14 * screen readers announce the bubble from the element that actually holds15 * focus (ARIA tooltip pattern), not from the positioning wrapper. */16 children: React.ReactElement17 /** Applied to the tooltip bubble, not the trigger wrapper. */18 className?: string19}2021export function AnimatedTooltip({ content, side = "top", children, className }: AnimatedTooltipProps) {22 const [hovered, setHovered] = React.useState(false)23 const [focused, setFocused] = React.useState(false)24 const reduced = useReducedMotion()25 const id = React.useId()26 const open = hovered || focused2728 const close = () => {29 setHovered(false)30 setFocused(false)31 }3233 // aria-describedby has to land on the element that actually takes focus (the34 // ARIA tooltip pattern); on the wrapper span a screen reader announces35 // nothing — hence injecting it via clone36 const trigger = React.cloneElement(children as React.ReactElement<Record<string, unknown>>, {37 "aria-describedby": open ? id : undefined,38 })3940 return (41 <span42 className="relative inline-flex"43 onBlur={() => setFocused(false)}44 onFocus={() => setFocused(true)}45 onKeyDown={e => {46 if (e.key === "Escape") close()47 }}48 onMouseEnter={() => setHovered(true)}49 onMouseLeave={() => setHovered(false)}50 >51 {trigger}52 <AnimatePresence>53 {open && (54 <motion.span55 animate={{56 opacity: 1,57 y: 0,58 scale: 1,59 transition: reduced ? { duration: 0.15 } : { type: "spring", stiffness: 300, damping: 18 },60 }}61 className={cn(62 "pointer-events-none absolute left-1/2 z-50 -translate-x-1/2 whitespace-nowrap rounded-md bg-foreground px-2.5 py-1 text-xs text-background shadow-md",63 side === "top" ? "bottom-full mb-2" : "top-full mt-2",64 className,65 )}66 exit={{ opacity: 0, transition: { duration: 0.15 } }}67 id={id}68// … truncated
What it pulls in
npm packages
Other registry items
Files it writes
More from zyeon
All 893 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| A/B Test Resultab-test-result | zyeon | Components | Paid | 2 deps · 2 files | |
| Accordionaccordion | zyeon | Components | Free | 1 dep | |
| Activity Feedactivity-feed | zyeon | Components | Free | 2 deps · 2 files | |
| Activity Heatmapactivity-heatmap | zyeon | Components | Paid | 1 dep · 2 files | |
| Address Formaddress-form | zyeon | Components | Paid | 1 dep | |
| Agent Graphagent-graph | zyeon | Components | Free | 2 deps · 2 files | |
| Agent Handoffagent-handoff | zyeon | Components | Free | 1 dep | |
| Agent Inboxagent-inbox | zyeon | Components | Free | 2 deps · 2 files |
