How to Implement Origin-Aware Animations that Scale from Trigger Elements in Skilled-Based UIs

To create origin-aware animations, store the trigger element's anchor point in a CSS custom property (--transform-origin) and apply it as the transform-origin for overlay elements before the animation starts.

Origin-aware animations make popovers, tooltips, and dropdowns appear to grow directly out of the element that opened them. This pattern, documented in emilkowalski/skills, eliminates the disconnected "floating" feel of center-scaled overlays and creates a stronger spatial relationship between user actions and UI responses.

Why Default Center Scaling Breaks the Experience

Most UI overlays anchor to a specific trigger element. When they scale from their own center, the animation feels psychologically disconnected from the user's click or hover action. The skills/emil-design-eng/SKILL.md file explicitly identifies this as a design violation: popovers should scale from their trigger, not from their geometric center 【emil-design-eng/SKILL.md#L34-L42】.

The only exception is modals, which are intentionally centered in the viewport and therefore correctly use the default center origin.

The Canonical CSS Pattern

According to skills/review-animations/SKILL.md, the solution uses a single CSS custom property that JavaScript populates dynamically 【review-animations/SKILL.md#L52-L53】:

.popover {
  transform-origin: var(--transform-origin);
  transform: scale(0.95);
  opacity: 0;
  transition: transform 150ms ease-out,
              opacity 150ms ease-out;
}

.popover[data-open] {
  transform: scale(1);
  opacity: 1;
}

This pattern satisfies the "origin & physical correctness" rule from the standards documentation in skills/review-animations/STANDARDS.md.

Calculating the Transform Origin at Runtime

The implementation requires measuring the trigger element and converting its position into CSS-compatible coordinates. Here's the complete workflow:

HTML Structure

<button id="menuBtn" class="trigger">Open Menu</button>

<div id="menuPopover" class="popover" aria-hidden="true">
  <ul>
    <li>Edit</li>
    <li>Duplicate</li>
    <li>Delete</li>
  </ul>
</div>

JavaScript Measurement and Activation

const trigger = document.getElementById('menuBtn');
const popover = document.getElementById('menuPopover');

trigger.addEventListener('click', () => {
  // 1. Capture trigger geometry in viewport coordinates
  const rect = trigger.getBoundingClientRect();

  // 2. Compute origin point (center used here; adjust corners as needed)
  const originX = rect.left + rect.width / 2;
  const originY = rect.top + rect.height / 2;

  // 3. Inject the origin into CSS before animation starts
  popover.style.setProperty('--transform-origin', `${originX}px ${originY}px`);

  // 4. Position the overlay relative to trigger
  popover.style.left = `${rect.left}px`;
  popover.style.top = `${rect.bottom}px`;

  // 5. Trigger the animation via attribute
  popover.dataset.open = '';
});

Key Implementation Details

  • Timing matters: Set --transform-origin before adding the activation class or attribute. CSS transitions evaluate the property at animation start; changing it mid-transition creates visual glitches.

  • Coordinate precision: getBoundingClientRect() returns subpixel-accurate floats. Pass these directly to CSS—browser rounding handles the rest.

  • Viewport-relative coordinates: The origin values are viewport-relative, which works correctly for absolutely positioned overlays. For fixed positioning inside transformed containers, adjust for offset ancestors.

  • Corner origins: For dropdown menus, use rect.left and rect.top directly to scale from the top-left corner instead of center:

popover.style.setProperty('--transform-origin', `${rect.left}px ${rect.top}px`);

Modals: The Deliberate Exception

Modals remain centered by design. The same skill file notes that transform-origin: center is correct for modals because they have no anchor element and appear in the viewport center 【emil-design-eng/SKILL.md#L34-L42】. Omit the custom property injection for modal implementations.

Rendering Pipeline Overview

Step Operation Critical File Reference
Measure getBoundingClientRect() on trigger Any component implementation
Compute Calculate anchor point (center/corner) Based on overlay type
Inject element.style.setProperty('--transform-origin', ...) review-animations/SKILL.md
Position Set left/top or use anchoring API Implementation-specific
Animate Toggle data-open or activation class CSS transition on transform/opacity

Summary

  • Default transform-origin: center violates physical correctness for anchored overlays.
  • The skills repository mandates var(--transform-origin) as the canonical solution.
  • Calculate trigger position via getBoundingClientRect(), inject into CSS, then animate.
  • Modals intentionally keep center origin; all other overlays require trigger-relative scaling.
  • Set the custom property before triggering the animation to prevent visual jumps.

Frequently Asked Questions

What happens if I forget to set --transform-origin before the animation starts?

The browser falls back to the default center value, breaking the origin-aware effect. The overlay scales from its geometric center rather than the trigger point, producing the "floating" disconnected animation that the review-animations skill explicitly flags as incorrect 【review-animations/SKILL.md#L52-L53】.

Can I use this pattern with CSS @keyframes instead of transitions?

Yes. The same --transform-origin injection works with @keyframes animations. Define your keyframes without explicit transform-origin values—the variable inheritance handles the dynamic origin point. Trigger the animation by adding a class that applies animation-name.

How do I handle origin-aware animations with React/Vue/Svelte?

Store the trigger rect in component state or a ref, compute the origin string in an effect or event handler, then bind the style property directly:

<div
  className="popover"
  style={{
    '--transform-origin': `${originX}px ${originY}px`
  }}
>

The architectural pattern from skills/review-animations/STANDARDS.md applies regardless of framework.

Does this work for exit animations as well?

Exit animations require the same origin to maintain physical consistency. Preserve the --transform-origin value set during entry; do not recalculate it when closing. If the trigger has moved (scrolled viewport), the exit will scale back to the original trigger position—often desirable for maintaining the user's spatial memory of the interaction.

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 →