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

> Learn to implement origin-aware animations that scale from trigger elements. Use CSS custom properties to control transform origin for dynamic UI effects. Read the guide now.

- Repository: [Emil Kowalski/skills](https://github.com/emilkowalski/skills)
- Tags: tutorial
- Published: 2026-08-05

---

**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`](https://github.com/emilkowalski/skills/blob/main/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](https://github.com/emilkowalski/skills/blob/main/skills/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`](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/SKILL.md), the solution uses a single CSS custom property that JavaScript populates dynamically 【[review-animations/SKILL.md#L52-L53](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/SKILL.md#L52-L53)】:

```css
.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`](https://github.com/emilkowalski/skills/blob/main/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

```html
<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

```javascript
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:

```javascript
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](https://github.com/emilkowalski/skills/blob/main/skills/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`](https://github.com/emilkowalski/skills/blob/main/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](https://github.com/emilkowalski/skills/blob/main/skills/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:

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

```

The architectural pattern from [`skills/review-animations/STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/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.