Had a hover card component in production. floating-ui, portal, resize observers, scroll listeners. ~400 lines. Standard tooltip positioning stack.
During a refactor I actually read through it and realized the browser already knows how to solve this.
Replaced the whole thing with layoutId from motion/react. Trigger avatar and card avatar share the same ID. Card opens, trigger unmounts, card mounts. Motion interpolates between the two positions automatically. Spring physics. No coordinate math. No portal.
{!open && (
<motion.div layoutId={`${uid}-av`} className="size-10 rounded-full overflow-hidden">
<Avatar />
</motion.div>
)}
// inside the expanded card
<motion.div layoutId={`${uid}-av`} className="size-10 rounded-full overflow-hidden">
<Avatar />
</motion.div>
Things that bit me:
Hover intent timers are non-negotiable. 80ms show, 100ms hide. Without this, moving across a row of avatars is a strobe light.
One layoutId = one DOM element. If both exist simultaneously the animation silently breaks. No error. Just a hard cut. Cost me an embarrassing amount of time.
Stagger your content reveals. ~60ms between each section. Individually invisible. Together it makes the card feel intentional instead of cheap.
Where it falls short: no viewport edge detection. If you need tooltip flipping, you still need floating-ui.
Result was ~160 lines. The diff deleted more code than it added.
Wrote a longer breakdown with a live demo here if anyone wants the full implementation.
Top comments (0)