How to Implement transform-origin Correctly for Popovers
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 (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:
/* 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 (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:
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 (lines 53-57), modals should retain transform-origin: center to maintain their centered, independent presence:
.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(lines 54-56): Defines the core rule that popovers must usevar(--transform-origin)rather thancenter.skills/emil-design-eng/SKILL.md(lines 48-49): Provides the design-engineering checklist documenting the transformation.skills/improve-animations/AUDIT.md(lines 53-57): Reinforces the origin-aware approach for consistency across the codebase.skills/find-animation-opportunities/SKILL.md: Identifies specific components where popovers need origin-aware animation.
Summary
- Never use
transform-origin: centerfor 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: centeronly 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.
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 →