# Best Practices for Modal vs Popover Transform Origins

> Learn best practices for modal vs popover transform origins. Ensure coherent animations by setting popover transform origin to the trigger center and modal transform origin to center.

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

---

**Set popover `transform-origin` to the trigger element's center using the `--transform-origin` CSS variable, while keeping modal `transform-origin` at `center` to ensure spatially coherent animations.**

When implementing animated overlays in the **emilkowalski/skills** repository, the choice of `transform-origin` directly impacts perceived spatial relationships and motion realism. According to the Animation Standards Reference documented in [`STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/STANDARDS.md), these two patterns require fundamentally different approaches to maintain visual consistency and user expectations.

## The Core Distinction

The emilkowalski/skills codebase establishes a strict dichotomy between anchored elements and viewport-centered overlays. This distinction is enforced during code reviews and documented in the "Physicality" section of [`STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/STANDARDS.md) and the "transform-origin" reference table in [`SKILL.md`](https://github.com/emilkowalski/skills/blob/main/SKILL.md).

### Popovers, Dropdowns, and Tooltips

**Anchored elements must scale from their trigger's center.** Popovers are spatially tied to the button or element that opened them, so their animation should communicate this relationship.

- Use `transform-origin: var(--transform-origin)` where the custom property calculates to the trigger's geometric center
- This creates a "source-to-target" motion that reinforces the mental model of the UI emerging from the user's click point
- Transition duration should be approximately **150ms** for these smaller, contextual elements

### Modals and Drawers

**Full-screen overlays must scale from the viewport center.** Unlike popovers, modals are not anchored to specific triggers; they appear as independent layers above the entire interface.

- Keep `transform-origin: center` to preserve symmetric, balanced entry animations
- This avoids implying false directional cues when no specific source element exists
- Use longer transitions (around **250ms**) to match the larger surface area and dramatic context shift

## Implementation Examples

The following patterns from the emilkowalski/skills repository demonstrate the correct application of these rules.

### CSS Implementation

```css
/* Popover – scales from the trigger position */
.popover {
  transform-origin: var(--transform-origin);
  transition: transform 150ms ease-out, opacity 150ms ease-out;
  transform: scale(0.95);
  opacity: 0;
}

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

/* Modal – scales from center */
.modal {
  transform-origin: center;
  transition: transform 250ms ease-out, opacity 250ms ease-out;
  transform: scale(0.95);
  opacity: 0;
}

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

```

### Dynamic Origin Calculation (React/TypeScript)

When using Base UI or similar libraries, calculate the `--transform-origin` property dynamically based on the anchor element's bounding box:

```tsx
import { useRef, useEffect } from "react";

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

  useEffect(() => {
    if (anchorRef.current && ref.current) {
      const rect = anchorRef.current.getBoundingClientRect();
      const originX = rect.left + rect.width / 2;
      const originY = rect.top + rect.height / 2;
      
      ref.current.style.setProperty(
        "--transform-origin", 
        `${originX}px ${originY}px`
      );
    }
  }, [anchorRef, open]);

  return (
    <div
      ref={ref}
      className="popover"
      data-open={open}
      role="dialog"
      aria-modal="false"
    >
      {children}
    </div>
  );
}

```

## Critical Implementation Notes

Avoid common pitfalls that break the spatial consistency established in the `emil-design-eng` standards:

- **Never use `scale(0)`** on entry; start from `scale(0.9)` to `scale(0.97)` combined with opacity fades to create natural-looking reveals
- **Do not override the origin** for modals; the "modals are exempt" rule is the only explicit exception to the trigger-origin pattern
- Ensure the `--transform-origin` variable is updated when the trigger moves or the viewport resizes to prevent misaligned animations

## Why the Distinction Matters

Understanding these rules requires recognizing how users perceive spatial relationships in interfaces.

**Spatial Consistency** – Users expect a popover to emerge from the button they pressed. If it scales from its own geometric center, the motion feels disjointed, breaking the cognitive link between trigger and result.

**Visual Balance** – Modals cover large areas and often dim the background. Center-origin scaling creates a symmetric, calm entrance appropriate for full-screen interruptions, avoiding the implication that the content originated from a specific screen location.

## Summary

- **Popovers** must use `transform-origin: var(--transform-origin)` pointing to the trigger's center to maintain spatial relationships
- **Modals** must use `transform-origin: center` to preserve visual balance and avoid false directional cues
- Always start scale animations from `0.95` or higher, never `0`, paired with opacity transitions
- Reference [`STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/STANDARDS.md) and [`SKILL.md`](https://github.com/emilkowalski/skills/blob/main/SKILL.md) (review-animations section) for the canonical implementation checklist

## Frequently Asked Questions

### Can I use `center` transform-origin for popovers if they appear near the trigger anyway?

No. According to the [`STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/STANDARDS.md) Physicality section, popovers must scale from the trigger's actual geometric center, not merely near it. Using `center` creates a subtle but perceptible disconnect between the user's click point and the animation origin, undermining the spatial mental model even when the popover is visually proximate.

### Why should I avoid starting animations from `scale(0)`?

Starting from `scale(0)` creates an unnatural "popping" effect that violates the physicality principles documented in the emilkowalski/skills repository. The human eye expects objects to have mass and volume; scaling from `0.9` or `0.95` with an opacity fade simulates a natural reveal, while `0` suggests the materialization of new matter.

### How do I calculate the trigger's center for the `--transform-origin` variable?

Calculate the trigger's center by adding half its width to its left offset and half its height to its top offset from the viewport. In JavaScript: `const originX = rect.left + rect.width / 2` and `const originY = rect.top + rect.height / 2`. Set these as the `--transform-origin` CSS custom property in the format `${originX}px ${originY}px` on the popover element.

### Where are these transform origin rules documented?

These guidelines are codified in [`STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/STANDARDS.md) under the Animation Standards Reference and Physicality sections, with practical checklists in [`SKILL.md`](https://github.com/emilkowalski/skills/blob/main/SKILL.md) under the review-animations and emil-design-eng sections. These files define the canonical approach for handling transform origins across the emilkowalski/skills codebase.