Best Practices for Transform-Origin in Popovers and Dropdowns
Set popover and dropdown animations to originate from their trigger element using a CSS custom property --transform-origin calculated via JavaScript, while keeping centered modals exempt with transform-origin: center.
Animations in anchored UI components should visually connect to the element that spawned them. According to the emilkowalski/skills repository standards, implementing transform-origin in popovers and dropdowns requires a declarative CSS variable approach coupled with precise coordinate calculations from the trigger's bounding box.
Why Transform-Origin Matters for Anchored Elements
Popovers and dropdowns are anchored to triggers—buttons, icons, or interactive controls. When these elements open, the scale animation must start from the trigger's position rather than the geometric center of the popover itself. This origin-aware animation clarifies the spatial relationship between trigger and content, creating a responsive and intuitive user experience that respects the user's point of interaction.
The CSS Custom Property Strategy
The Skills repository codifies this pattern in skills/review-animations/STANDARDS.md. The standard mandates using a CSS custom property --transform-origin that is dynamically set to the trigger's coordinates.
Base CSS Configuration
In skills/review-animations/STANDARDS.md (lines 56-58), the standard defines the base rule:
.popover {
transform-origin: var(--transform-origin);
}
For defensive CSS that handles edge cases, include a fallback to center:
.popover,
.dropdown {
transform-origin: var(--transform-origin, center);
opacity: 0;
transform: scale(0.95);
transition: opacity 150ms var(--ease-out), transform 150ms var(--ease-out);
}
.popover[data-open],
.dropdown[data-open] {
opacity: 1;
transform: scale(1);
}
The var(--ease-out) custom easing referenced above is defined in skills/review-animations/STANDARDS.md (lines 31-34).
The Modal Exception
Modals are centered overlays that appear independently of specific triggers. According to skills/emil-design-eng/SKILL.md, modals should maintain transform-origin: center, distinguishing them from anchored popovers and dropdowns that require dynamic origin calculation.
Implementation Architecture
The implementation consists of three coordinated parts: defining the CSS baseline, calculating trigger coordinates, and injecting the value before the transition runs.
Calculating Trigger Coordinates
Before opening the popover, use getBoundingClientRect() to determine the center point of the trigger element. As documented in the design notes of skills/emil-design-eng/SKILL.md, this calculation converts viewport coordinates into the animation origin point:
const { left, top, width, height } = trigger.getBoundingClientRect();
const originX = left + width / 2;
const originY = top + height / 2;
Injecting the CSS Variable
Write the calculated coordinates to the popover's style property using setProperty() before toggling the open state. This must occur before the CSS transition begins:
popover.style.setProperty('--transform-origin', `${originX}px ${originY}px`);
popover.setAttribute('data-open', '');
Complete Implementation Examples
Here is a runnable implementation combining the CSS standards from skills/review-animations/STANDARDS.md with JavaScript coordinate calculation:
/**
* Opens a popover with origin-aware animation.
* @param {HTMLElement} trigger - The button or element that triggers the popover
* @param {HTMLElement} popover - The popover element to animate
*/
function openPopover(trigger, popover) {
// Calculate center of trigger relative to viewport
const { left, top, width, height } = trigger.getBoundingClientRect();
const originX = left + width / 2;
const originY = top + height / 2;
// Apply the transform origin via CSS custom property
popover.style.setProperty('--transform-origin', `${originX}px ${originY}px`);
// Trigger the CSS transition via data attribute
popover.setAttribute('data-open', '');
}
// Event listener setup
document.querySelectorAll('.trigger').forEach(button => {
const popover = document.getElementById(button.dataset.popoverId);
button.addEventListener('click', () => openPopover(button, popover));
});
For dropdowns, apply the same principle using toggleAttribute():
function toggleDropdown(trigger, dropdown) {
const { left, top, width, height } = trigger.getBoundingClientRect();
const originX = left + width / 2;
const originY = top + height / 2;
dropdown.style.setProperty('--transform-origin', `${originX}px ${originY}px`);
dropdown.toggleAttribute('data-open');
}
Both snippets compute the center point of the trigger and expose it to the CSS via --transform-origin. The CSS transition then animates from that point, satisfying the "origin-aware popovers" rule documented in skills/review-animations/SKILL.md.
Key Implementation Files
The following files in the emilkowalski/skills repository define the standards and implementation patterns for transform-origin in popovers and dropdowns:
skills/review-animations/STANDARDS.md- Official animation standards including the--transform-originrule and easing variable definitions (lines 31-34, 56-58)skills/review-animations/SKILL.md- Concise summary table converting hardcodedtransform-origin: centertovar(--transform-origin)for popoversskills/emil-design-eng/SKILL.md- Design engineering notes explaining why modals are exempt and how to apply the variable patternskills/improve-animations/AUDIT.md- Audit checklist reinforcing the implementation pattern for popoversskills/find-animation-opportunities/SKILL.md- Guidelines for identifying where "origin-aware" animations are needed
Summary
- Use
transform-origin: var(--transform-origin, center)in CSS for popovers and dropdowns to enable dynamic origin points calculated at runtime - Calculate the trigger's center using
getBoundingClientRect()and write it to the CSS variable viaelement.style.setProperty()before opening the element - Reserve
transform-origin: centerexclusively for centered modals, not anchored elements like popovers or dropdowns - Reference
skills/review-animations/STANDARDS.mdin the emilkowalski/skills repository for official easing variables and animation timing - Apply the
data-openattribute to trigger CSS transitions only after setting the origin variable to ensure the animation starts from the correct point
Frequently Asked Questions
Why not hardcode transform-origin values in CSS for each popover?
Hardcoding requires knowing the trigger position in advance, which is impossible with responsive layouts, dynamic trigger placement, or scrollable containers. Using the --transform-origin CSS custom property calculated at runtime via JavaScript ensures the animation originates from the actual trigger location regardless of viewport changes, as specified in skills/review-animations/STANDARDS.md.
What is the difference between popover and modal transform-origin behavior?
According to skills/emil-design-eng/SKILL.md, popovers and dropdowns are anchored to specific triggers and must use transform-origin: var(--transform-origin) pointing to that trigger's coordinates. Modals are centered overlays that appear in the middle of the viewport and should maintain transform-origin: center for symmetrical expansion from the center of the screen.
When should I calculate and set the --transform-origin variable?
Calculate and set the variable immediately before opening the popover or dropdown. In the implementation pattern from skills/review-animations/STANDARDS.md, this occurs between reading getBoundingClientRect() from the trigger and adding the data-open attribute that triggers the CSS transition. Setting the variable after the transition starts will cause the animation to begin from the wrong origin.
Does this approach work with React or other frameworks?
Yes. The pattern is framework-agnostic. In React, use useRef to access the DOM nodes and element.style.setProperty('--transform-origin', value) to set the variable before calling setState or toggling an isOpen boolean. The CSS custom property approach decouples the animation logic from component state, making it portable across React, Vue, Svelte, or vanilla JavaScript implementations.
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 →