How to Implement transform-origin Correctly for Popovers

Use a CSS custom property (e.g., --transform-origin) set to the trigger element's position instead of the default center value, ensuring popovers scale from their anchor point rather than their geometric center.

Popovers, tooltips, and dropdown menus require precise animation origins to maintain spatial consistency with their trigger elements. The emilkowalski/skills repository codifies this principle in its animation standards, demonstrating that anchoring animations to the trigger point creates a stronger mental model and improves perceived performance. Implementing this correctly involves replacing the default transform-origin: center with a dynamic custom property calculated from the trigger's coordinates.

Why transform-origin Matters for Popover Animations

The default CSS rule transform-origin: center forces popovers to scale from their geometric center, making them appear to grow out of thin air. This breaks spatial consistency—the visual relationship between the button and the popover becomes ambiguous, forcing users to mentally reconnect the floating element with its trigger.

When you tie the popover's origin to the trigger position, the animation reinforces that "the popover belongs to this control." As documented in skills/review-animations/STANDARDS.md (lines 54-56), this approach also improves perceived speed because the motion starts immediately under the user's focus, eliminating the cognitive disconnect caused by center-scaling animations.

The CSS Custom Property Approach

Base UI libraries in the emilkowalski/skills repository expose a --transform-origin variable that popovers inherit automatically. This variable should be set to the trigger's position rather than hardcoded to center.

Static CSS Implementation

For simple cases where the trigger position is known, set the custom property directly on the trigger element:

/* Base UI – applied to every popover */
.popover {
  transform-origin: var(--transform-origin);
}

/* Example trigger that sets the variable */
.button {
  --transform-origin: left top; /* or any calculated point */
}

This pattern appears in skills/emil-design-eng/SKILL.md (lines 48-49), which explicitly documents the "before → after" transformation from center to var(--transform-origin).

Dynamic Calculation with JavaScript/React

For dynamic positioning, calculate the trigger's center point and set the property programmatically:

import { useRef, useEffect } from 'react';

function Popover({ anchorRef, children }) {
  const popRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!anchorRef.current || !popRef.current) return;
    const rect = anchorRef.current.getBoundingClientRect();
    const origin = `${rect.left + rect.width / 2}px ${rect.top + rect.height / 2}px`;
    popRef.current.style.setProperty('--transform-origin', origin);
  }, [anchorRef]);

  return (
    <div ref={popRef} className="popover">
      {children}
    </div>
  );
}

This implementation ensures the popover scales from the exact center of the trigger element regardless of scroll position or viewport changes.

The Modal Exception: When to Keep transform-origin: center

Not all floating elements should use trigger-based origins. Modals are not anchored to a specific trigger—they are centered viewport elements that demand attention to themselves. According to skills/improve-animations/AUDIT.md (lines 53-57), modals should retain transform-origin: center to maintain their centered, independent presence:

.modal {
  /* Modals are centered, so keep the default */
  transform-origin: center;
}

This distinction separates anchored components (popovers, tooltips, menus) from viewport-centered components (modals, dialogs).

Source Code References

The implementation guidelines appear consistently across the repository:

Summary

  • Never use transform-origin: center for popovers, tooltips, or dropdown menus that anchor to a specific trigger element.
  • Always use transform-origin: var(--transform-origin) combined with a CSS custom property set to the trigger's position.
  • Calculate dynamically when trigger positions change based on viewport or scroll using getBoundingClientRect().
  • Keep transform-origin: center only for modals and other viewport-centered elements that lack a specific anchor point.

Frequently Asked Questions

What is the default transform-origin behavior?

By default, CSS applies transform-origin: center to all transformed elements, meaning animations scale from the element's geometric center. For popovers, this causes the element to appear growing from its middle rather than from the trigger point, creating a spatial disconnect.

Why shouldn't popovers use transform-origin: center?

Using center for popovers breaks the mental model of the UI component. When a user clicks a button, they expect the resulting popover to emanate from that button. Center-scaling makes the popover appear disconnected and "slower" because the motion doesn't start where the user's attention is focused.

How do I calculate the trigger position for dynamic origins?

Use the trigger element's getBoundingClientRect() method to determine its viewport position, then calculate the center point: ${rect.left + rect.width / 2}px ${rect.top + rect.height / 2}px. Set this value to a CSS custom property (e.g., --transform-origin) on the popover element before animation begins.

Are there exceptions to this rule?

Yes. Modals and other viewport-centered dialogs should retain transform-origin: center because they are not anchored to specific trigger elements. As noted in the repository's audit documentation, only anchored components like popovers, tooltips, and menus require the custom origin approach.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →