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.

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 in skills/review-animations/STANDARDS.md.
  • Calculate trigger center: Use getBoundingClientRect() to find the trigger's viewport position, then compute left + width/2 and top + 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: center for modals since they have no anchor element, as noted in skills/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:

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 →