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
skills/review-animations/STANDARDS.md— Defines the physicality rule and CSS snippet for popoversskills/review-animations/SKILL.md— Reviewer checklist with explicit good/bad examplesskills/emil-design-eng/SKILL.md— Modal exception documentationskills/improve-animations/AUDIT.md— Automated detection of incorrect usageskills/improve-animations/PLAN-TEMPLATE.md— Structured fix implementation guide
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: centersince they have no trigger attachment - Never animate from
scale(0)—start from0.90–0.97for physical realism - Repository tooling enforces these rules through automated audits
- Reference
emil-design-eng/SKILL.mdandreview-animations/STANDARDS.mdfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →