# How to Implement transform-origin Correctly for Popovers

> Learn to implement transform-origin correctly for popovers. Scale popovers from their anchor point using CSS custom properties for precise positioning and a better user experience.

- Repository: [Emil Kowalski/skills](https://github.com/emilkowalski/skills)
- Tags: how-to-guide
- Published: 2026-08-01

---

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

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

```tsx
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`](https://github.com/emilkowalski/skills/blob/main/skills/improve-animations/AUDIT.md) (lines 53-57), modals should retain `transform-origin: center` to maintain their centered, independent presence:

```css
.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:

- **[`skills/review-animations/STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md)** (lines 54-56): Defines the core rule that popovers must use `var(--transform-origin)` rather than `center`.
- **[`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md)** (lines 48-49): Provides the design-engineering checklist documenting the transformation.
- **[`skills/improve-animations/AUDIT.md`](https://github.com/emilkowalski/skills/blob/main/skills/improve-animations/AUDIT.md)** (lines 53-57): Reinforces the origin-aware approach for consistency across the codebase.
- **[`skills/find-animation-opportunities/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/find-animation-opportunities/SKILL.md)**: Identifies specific components where popovers need origin-aware animation.

## 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.