Why `transform-origin: center` Is Wrong for Popovers (And What to Use Instead)
transform-origin: center breaks the spatial relationship between a popover and its trigger, making the animation appear disconnected; popovers should instead use a trigger-aware origin via the custom property --transform-origin.
Popovers—tooltips, menus, dropdowns—are trigger-anchored UI elements. They exist because a user clicked or hovered a specific control, and their animation should visually communicate that relationship. In the emilkowalski/skills repository, the design system explicitly rejects transform-origin: center for these components. This article explains why, based on the Base UI guidelines documented throughout the repo.
The Problem with transform-origin: center
When you set transform-origin: center on a popover, the scaling animation originates from the element's own geometric midpoint. The result is a jarring effect: the popover appears to materialize from nowhere rather than emerging from the button that spawned it.
This violates a fundamental principle of UI motion: visual continuity. Users perceive animated elements as continuations of the interactions that caused them. A disconnected origin breaks that mental model.
The Base UI Solution: --transform-origin
The repository's design system solves this with a runtime-computed custom property. As documented in skills/emil-design-eng/SKILL.md (line 236), the correct approach is:
.popover {
transform-origin: var(--transform-origin);
}
The --transform-origin property is calculated from the trigger element's position and applied dynamically. This ensures the popover scales from the point where the user initiated the interaction.
How the Calculation Works
The JavaScript derives the origin coordinates relative to the popover's position:
const trigger = document.getElementById('trigger');
const popover = document.getElementById('myPopover');
trigger.addEventListener('click', () => {
const rect = trigger.getBoundingClientRect();
const originX = rect.left + rect.width / 2 - popover.offsetLeft;
const originY = rect.top + rect.height / 2 - popover.offsetTop;
popover.style.setProperty('--transform-origin', `${originX}px ${originY}px`);
popover.toggleAttribute('data-open');
});
The trigger's center point is computed, offset by the popover's position, then injected into the CSS custom property. The animation now scales from that precise point.
Complete Implementation Example
Here's a production-ready popover with proper origin-aware scaling:
<button id="trigger">Open Menu</button>
<div class="popover" id="myPopover">
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</div>
.popover {
/* Base UI pattern: fallback to center if custom property unavailable */
transform-origin: var(--transform-origin, center);
transform: scale(0);
opacity: 0;
transition:
transform 0.15s ease-out,
opacity 0.1s ease-out;
position: absolute;
}
.popover[data-open] {
transform: scale(1);
opacity: 1;
}
The var(--transform-origin, center) syntax provides graceful degradation. If the custom property isn't set—whether due to script failure or an unsupported environment—the popover still functions with a centered origin.
The Exception: Modals
Modals are not trigger-anchored. They appear centered in the viewport, disconnected from any specific control. For these components, transform-origin: center remains appropriate.
As noted in skills/animate/SKILL.md (lines 79-82), this exemption is intentional:
.modal {
/* Modals centered in viewport: default origin is correct */
transform-origin: center;
/* ...other modal styles... */
}
The distinction hinges on spatial anchoring. Popovers maintain a relationship to their trigger; modals assert independence from the underlying interface.
Architectural Rationale
The transform-origin: center prohibition in emilkowalski/skills rests on three pillars documented across multiple skill files:
-
Spatial consistency — Motion perceived as continuous with user action reinforces causality and predictability.
-
Accessibility and predictability — Consistent origin placement reduces cognitive load for assistive technology users and minimizes motion-sickness triggers.
-
Design system uniformity — The
--transform-originvariable provides a single source of truth, enforced through standards inskills/review-animations/STANDARDS.md(lines 54-56).
Source File Reference
| File | Relevant Content |
|---|---|
skills/emil-design-eng/SKILL.md (line 236) |
"Make popovers origin-aware" — explains the problem and introduces var(--transform-origin) |
skills/review-animations/STANDARDS.md (lines 54-56) |
"Origin-aware popovers" — codifies the CSS rule in animation standards |
skills/animate/SKILL.md (lines 79-82) |
Reinforces rule and modal exemption |
skills/review-animations/SKILL.md (lines 52-53) |
Diagnostic discussion of center origin problems |
skills/improve-animations/AUDIT.md (lines 53-56) |
Concrete CSS snippet for audits |
Summary
transform-origin: centerdisconnects popover animations from their triggers, creating disjointed UX.- Trigger-aware origins via
var(--transform-origin)preserve spatial relationships and user mental models. - JavaScript calculates the origin from trigger coordinates at runtime.
- Modals are exempt because they lack trigger anchoring.
- The Base UI pattern with fallback syntax ensures robust, progressive enhancement.
Frequently Asked Questions
Should I never use transform-origin: center?
Not never—just not for trigger-anchored components. Popovers, tooltips, dropdowns, and menus all benefit from trigger-aware origins. Modals, toast notifications, and other viewport-centered elements can safely use center.
How does --transform-origin get its value?
JavaScript computes it from the trigger element's getBoundingClientRect(), adjusts for the popover's position, and sets the custom property via element.style.setProperty(). UI libraries like those referenced in this repo automate this calculation.
What happens if JavaScript fails to set the property?
The fallback value var(--transform-origin, center) ensures the animation still works. The origin won't be trigger-aware, but the component remains functional—a core tenet of progressive enhancement.
Is this pattern specific to this repository's design system?
The --transform-origin naming convention and specific implementation derive from the Base UI system in emilkowalski/skills. However, the underlying principle—matching transform origin to user interaction point—applies universally across modern UI development.
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 →