Micro-interactions

Tether

A popover that grows out of the corner nearest its trigger, and moves that origin when it flips.

Install

bun add motion clsx tailwind-merge

Then copy 2 files into your project:

  • components/registry/tether/tether.tsx
  • lib/utils.ts

Props

PropTypeDefaultDescription
side"top" | "right" | "bottom" | "left""bottom"Preferred side. The transform origin is derived from the side the panel actually resolved to, never this one — a flip that keeps the requested origin grows the panel away from the trigger it just moved to.
align"start" | "center" | "end""center"Where the panel sits along that side. It moves the origin and the caret together, so a start-aligned panel below the trigger grows down and to the right out of the button's left corner.
offsetnumber10Gap between trigger and panel. Padding rather than margin: a pointer crossing the gap on its way into a hover-opened panel never leaves the component, so the panel does not close under the cursor.
stiffnessnumber520Spring tension on the pop. High enough to feel like the panel is already there by the time the eye arrives.
dampingnumber26Spring friction. A little overshoot is the pop; too little and the panel wobbles, which on a menu reads as unresponsive rather than lively.
popScalenumber0.86Size the panel grows from. At 1 the origin work is invisible and the entrance is a plain fade — worth setting once to see what the rest of the component is buying.
arrowbooleantrueCaret on the tethered edge. It sits where the panel is anchored, so it stays pointing at the trigger through a side change without any repositioning code.
flipbooleantrueMove to the opposite side when the preferred one would overflow the viewport. The fit test reads the panel's layout box rather than its bounding rect, because the rect carries the entrance scale and would decide using a box smaller than the one about to be there.
openOn"click" | "hover""click"What opens the panel. Click also moves focus into it; hover deliberately does not, and answers focus on the trigger instead so the keyboard is not locked out of a pointer-only affordance.
closeDelaynumber140Grace period before a hover-opened panel closes. The timer also refuses to fire while focus is still inside, so a pointer wandering off cannot destroy the element someone is tabbing through.
radiusnumber10Panel corner radius.
accentColorstring (hex)"#a855f7"The rule on the tethered edge and the caret. Both move with a flip, so the panel keeps pointing back at its origin on whichever side it ended up.

What It Demonstrates

Pop in
Element appears with a slight overshoot, like it bounces into place.
Origin-aware animation
An element animates out of its trigger, like a popover growing from the button that opened it instead of from its own center which is the default in CSS.
Transform origin
The anchor point a scale or rotation grows or spins from.
Enter / Exit
The animation an element plays when it's added to or removed from the screen.
Spring
Motion driven by physics (tension, mass, damping) rather than a set duration.

Related Components

Appears In