Transform-Origin Best Practices for Popovers, Dropdowns, and Modals in the Skills Repository

Use transform-origin: var(--transform-origin) anchored to the trigger element for popovers and dropdowns, but keep transform-origin: center for centered modals.

The transform-origin CSS property controls the pivot point for scale and rotate animations. In the emilkowalski/skills repository, this property is governed by strict animation standards that prioritize physical realism and spatial consistency. Following these conventions ensures UI elements appear to grow naturally from their triggers rather than animating from arbitrary points.

Why Transform-Origin Matters for Anchored UI Components

When a popover or dropdown opens, users mentally associate it with the button or element that triggered it. The animation should reinforce this relationship.

transform-origin set to the trigger's position creates the illusion that the component materializes from that trigger. This aligns with how objects behave in the physical world—things expand from where they originate, not from their own geometric center.

The Skills repository codifies this principle across multiple documentation files and provides concrete implementation patterns.

The Core Rule: Trigger-Anchored Origins

According to STANDARDS.md in the review-animations skill, the Base UI implements this rule with a CSS variable:

.popover {
  transform-origin: var(--transform-origin);
}

The --transform-origin variable is dynamically populated with coordinates relative to the trigger element's bounding box [L53-L58].

The same standards explicitly mark this as incorrect: transform-origin: center on a trigger-anchored popover. The correct approach uses the variable-driven origin [L31-L52].

How the Variable Gets Populated

The trigger element sets --transform-origin when opening the popover. Here's the pattern from the repository's implementation guidance:

function openPopover(trigger, popover) {
  const rect = trigger.getBoundingClientRect();
  const originX = rect.left + rect.width / 2;
  const originY = rect.top + rect.height / 2;
  popover.style.setProperty('--transform-origin',
    `${originX}px ${originY}px`);
  popover.dataset.open = '';
}

This calculates the trigger's center and writes it to the CSS custom property, ensuring the popover scales from the correct point.

Complete Popover/Dropdown Implementation

Combine transform-origin with proper scale and opacity handling for physically realistic entry:

.popover {
  /* Origin follows the trigger position */
  transform-origin: var(--transform-origin);
  /* Start slightly reduced—never scale(0), which would be physically impossible */
  transform: scale(0.95);
  opacity: 0;
  transition: transform 180ms var(--ease-out), opacity 180ms var(--ease-out);
}

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

Critical detail from the standards: Never use scale(0) for entry animations. No physical object appears from absolute nothing—it creates a jarring, discontinuous jump. The repository recommends scale(0.90–0.97) as the starting range.

The Modal Exception: Centered Origins

Modals break the trigger-anchored rule intentionally. As documented in emil-design-eng/SKILL.md [L236-L242], modals are deliberately centered in the viewport and unattached to any specific trigger. Applying a trigger-based origin would misalign the animation with the visual reality.

.modal {
  /* Modals stay centered, so origin matches the element center */
  transform-origin: center;
  transform: scale(0.95);
  opacity: 0;
  transition: transform 250ms var(--ease-out), opacity 250ms var(--ease-out);
}

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

The longer transition duration (250ms vs. 180ms) accommodates the larger spatial movement and maintains perceived performance.

Common Transform-Origin Mistakes

Mistake Problem Repository-Correct Approach
transform-origin: center on popovers Breaks spatial link to trigger; animation appears disconnected Use var(--transform-origin) set by trigger
transform: scale(0) for entry Physically unrealistic; creates jarring pop-in Start from scale(0.95) with opacity: 0
Applying modal exception to popovers Misplaces origin, causing offset scaling Reserve center for true modals only
Hardcoding origin values Loses responsiveness to trigger position changes Use JavaScript-calculated CSS variables

Automated Enforcement

The improve-animations audit tool in the Skills repository automatically detects transform-origin misuse. As specified in AUDIT.md [L53-L58], the script flags popover components that still use center instead of the variable-driven approach.

This tooling ensures standards propagate consistently across large codebases without relying solely on manual code review.

Key Reference Files in emilkowalski/skills

Summary

  • Popovers, dropdowns, and menus must use transform-origin: var(--transform-origin) dynamically set to the trigger element's position
  • Modals are the exception: use transform-origin: center since they have no trigger attachment
  • Never animate from scale(0)—start from 0.90–0.97 for physical realism
  • Repository tooling enforces these rules through automated audits
  • Reference emil-design-eng/SKILL.md and review-animations/STANDARDS.md for implementation specifics

Frequently Asked Questions

How do I calculate the correct transform-origin for a dynamic trigger position?

Calculate the trigger element's center using getBoundingClientRect(), then set the CSS variable before showing the popover. The repository's JavaScript pattern in review-animations/SKILL.md demonstrates this with rect.left + rect.width / 2 for horizontal positioning.

Why can't I use transform-origin: center for everything?

Centered origins disconnect the animation from the trigger's spatial location. For anchored components, this breaks the mental model users form when clicking a button. The Skills repository explicitly flags this as incorrect in animation reviews.

What timing values does the Skills repository recommend for these animations?

Popovers and dropdowns use 180ms transitions; modals use 250ms. Both use --ease-out easing. These values balance responsiveness with perceived physicality, as documented across the animation standards files.

Does the repository provide tools to catch transform-origin mistakes automatically?

Yes. The improve-animations audit script scans code for transform-origin: center on popover-class components and flags violations. This is documented in skills/improve-animations/AUDIT.md with implementation guidance in PLAN-TEMPLATE.md.

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 →