How to Implement transform-origin for Popovers and Dropdowns
Set a CSS custom property --transform-origin to the trigger element's center coordinates via JavaScript before opening the popover, allowing the scale animation to originate from the trigger's position rather than the element's geometric center.
Popovers and dropdowns are UI elements anchored to a trigger such as a button or icon. According to the animation standards in the emilkowalski/skills repository, these components should animate from the trigger's position to maintain spatial continuity. This technique, codified in skills/review-animations/STANDARDS.md, uses a dynamic CSS custom property to bridge JavaScript calculations with CSS transitions.
The CSS Foundation Using Custom Properties
The baseline CSS establishes a shared animation contract through the --transform-origin variable. In skills/review-animations/STANDARDS.md (lines 56-58), the standard mandates that popovers read from this variable rather than using a static value.
Define the transform origin to use the variable, with a safe fallback to center:
/* Base UI standard from STANDARDS.md */
.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);
}
/* Open state triggers the animation */
.popover[data-open],
.dropdown[data-open] {
opacity: 1;
transform: scale(1);
}
The var(--ease-out) reference corresponds to the easing curves defined in lines 31-34 of the same standards file. By keeping the origin declarative via CSS variables, any component following the .popover or .dropdown class convention automatically inherits origin-aware behavior without duplicating JavaScript logic.
Calculating the Trigger Position
Before the popover opens, JavaScript must compute the trigger's center point and inject it into the element's style. The skills/emil-design-eng/SKILL.md file outlines this pattern: read the trigger's bounding box, calculate the center, and write the value to --transform-origin.
/**
* Opens a popover with origin-aware animation.
* Calculates trigger center and sets --transform-origin before the transition runs.
*
* @param {HTMLElement} trigger - The element that activated the popover
* @param {HTMLElement} popover - The popover element to animate
*/
function openPopover(trigger, popover) {
// Get trigger dimensions relative to viewport
const { left, top, width, height } = trigger.getBoundingClientRect();
// Calculate center point
const originX = left + width / 2;
const originY = top + height / 2;
// Inject into CSS custom property
popover.style.setProperty('--transform-origin', `${originX}px ${originY}px`);
// Trigger the CSS transition by setting the open state
popover.setAttribute('data-open', '');
// Accessibility: manage focus and aria-expanded here
}
This calculation uses getBoundingClientRect to obtain viewport-relative coordinates, ensuring the origin point remains accurate even if the user has scrolled the page.
Dropdown Implementation and Modal Exceptions
Dropdowns follow the identical pattern. The skills/review-animations/SKILL.md notes that while popovers and dropdowns use dynamic origins, modals remain centered and should explicitly use transform-origin: center since they lack a specific trigger anchor.
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`);
const isOpen = dropdown.hasAttribute('data-open');
dropdown.toggleAttribute('data-open', !isOpen);
}
The architectural flow remains consistent across both components: compute coordinates, set the property, then toggle visibility. This separation of concerns keeps the animation logic in CSS while the positioning logic lives in JavaScript.
Summary
- Use CSS variables: Define
transform-origin: var(--transform-origin, center)in your base popover and dropdown styles as specified inskills/review-animations/STANDARDS.md. - Calculate trigger center: Use
getBoundingClientRect()to find the trigger's viewport position, then computeleft + width/2andtop + height/2. - Set before animating: Call
element.style.setProperty('--transform-origin', ...)before adding the open state or class that triggers CSS transitions. - Center modals: Keep
transform-origin: centerfor modals since they have no anchor element, as noted inskills/emil-design-eng/SKILL.md. - Maintain standards: Reference the easing functions and timing constants defined in the repository's animation standards for consistent motion.
Frequently Asked Questions
Why can't I just use transform-origin: center for popovers?
Using center causes the popover to expand from its own geometric middle, which breaks the spatial relationship with the trigger button. According to the principles in skills/review-animations/STANDARDS.md, anchoring the animation origin to the trigger's position creates a more responsive feel and clarifies the connection between the control and its resulting panel.
How do I handle transform-origin when the popover appears above the trigger?
The calculation remains the same regardless of final placement. Calculate the trigger's center coordinates using getBoundingClientRect(), and if your positioning logic places the popover above the trigger, the CSS transform-origin will still point to the correct viewport location. The origin calculation is independent of the popover's final top or left position.
Should dropdown menus use the same transform-origin technique?
Yes. The skills/find-animation-opportunities/SKILL.md explicitly includes dropdown menus, panels, and popovers as candidates for origin-aware animations. Apply the same JavaScript calculation and CSS variable pattern to dropdowns to maintain consistency across your component library.
What happens if I forget to set the --transform-origin variable?
The CSS fallback var(--transform-origin, center) ensures graceful degradation. If the JavaScript fails to set the property, the animation simply originates from the element's center rather than the trigger's position, maintaining functionality while losing the enhanced spatial effect.
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 →