Skip to content

fix: popup invisible due to CSSTransition interfering with measurement#639

Open
baofuen wants to merge 1 commit into
react-component:masterfrom
baofuen:fix/popup-position-measurement
Open

fix: popup invisible due to CSSTransition interfering with measurement#639
baofuen wants to merge 1 commit into
react-component:masterfrom
baofuen:fix/popup-position-measurement

Conversation

@baofuen

@baofuen baofuen commented Jul 13, 2026

Copy link
Copy Markdown

fix: popup invisible due to CSSTransition interfering with measurement (#618)

Problem

Popup (Dropdown/Select/Tooltip/etc.) renders off-screen and is invisible
when the host page applies a non-zero transition-duration to left/top
on the popup element. This is caused by two widely-deployed CSS patterns:

  1. The accessibility rule recommended by WCAG:
    @media (prefers-reduced-motion: reduce) { * { transition-duration:
    0.01ms !important } } -- active in Chromium headless, VS Code Webview,
    DevTools emulation, and OS-level reduced-motion.

  2. React 19 serialises top/left/right/bottom into the inset shorthand,
    which can start a CSSTransition on the longhand properties even without
    explicit prefers-reduced-motion.

Root cause

useAlign resets popupElement.style.left = '0' / style.top = '0' to
measure the popup at the origin of its offsetParent, then synchronously
reads getBoundingClientRect(). Per CSS Transitions Level 1 section 3.1,
Animations origin sits above Author Important origin in the cascade, so
at t=0 of a just-started CSSTransition getBoundingClientRect() reports
the previous used value (-1000vw/-1000vh from useOffsetStyle) instead
of the newly-assigned 0. The offset math then explodes
(e.g. triggerRect.bottom - (-7400) = 7438) and the popup renders
thousands of pixels off-screen -- from the user's perspective, clicking
the trigger "does nothing".

Additionally, onVisibleChanged calls onAlign() a second time after
motion completes. This re-measures the popup using the first run's offset
as the baseline (because the popup has already been moved), producing an
oscillating offset: first run computes +41px, second run reads the
popup at top: 41px and computes 0px, snapping back.

Fix

  1. useAlign.ts: Temporarily set popupElement.style.transition = 'none'
    for the duration of the measurement, restoring it afterwards. This
    prevents CSSTransitions from starting on left/top, so the inline
    style.left = '0' / style.top = '0' writes take effect immediately
    and getBoundingClientRect() reports the correct origin.

  2. index.tsx: Remove the redundant onAlign() call in
    onVisibleChanged. The initial alignment is already performed via the
    motionPrepareResolve useLayoutEffect path; the second call causes
    the oscillation described above.

Verification

  • tsc --noEmit passes with no errors.
  • Reproduced and verified the fix against a production app using
    antd@6.3.1 (depends on @rc-component/trigger@3.9.0) + React 19.2:
    • Before fix: popup at top: -7343px (invisible) on every open.
    • After fix: popup at top: 41px (trigger.bottom + offset), visible
      and interactive.

Fixes #618

Summary by CodeRabbit

  • Bug Fixes
    • 优化弹层定位测量,减少过渡动画导致的位置计算偏差。
    • 修复弹层显示或切换时可能出现的对齐抖动、位置振荡问题。
    • 提升弹层打开、翻转及偏移定位的稳定性。

react-component#618)

## Problem

Popup (Dropdown/Select/Tooltip/etc.) renders off-screen and is invisible
when the host page applies a non-zero transition-duration to left/top
on the popup element. This is caused by two widely-deployed CSS patterns:

1. The accessibility rule recommended by WCAG:
   @media (prefers-reduced-motion: reduce) { * { transition-duration:
   0.01ms !important } } -- active in Chromium headless, VS Code Webview,
   DevTools emulation, and OS-level reduced-motion.

2. React 19 serialises top/left/right/bottom into the inset shorthand,
   which can start a CSSTransition on the longhand properties even without
   explicit prefers-reduced-motion.

## Root cause

useAlign resets popupElement.style.left = '0' / style.top = '0' to
measure the popup at the origin of its offsetParent, then synchronously
reads getBoundingClientRect(). Per CSS Transitions Level 1 section 3.1,
Animations origin sits above Author Important origin in the cascade, so
at t=0 of a just-started CSSTransition getBoundingClientRect() reports
the previous used value (-1000vw/-1000vh from useOffsetStyle) instead
of the newly-assigned 0. The offset math then explodes
(e.g. triggerRect.bottom - (-7400) = 7438) and the popup renders
thousands of pixels off-screen -- from the user's perspective, clicking
the trigger "does nothing".

Additionally, onVisibleChanged calls onAlign() a second time after
motion completes. This re-measures the popup using the first run's offset
as the baseline (because the popup has already been moved), producing an
oscillating offset: first run computes +41px, second run reads the
popup at top: 41px and computes 0px, snapping back.

## Fix

1. useAlign.ts: Temporarily set popupElement.style.transition = 'none'
   for the duration of the measurement, restoring it afterwards. This
   prevents CSSTransitions from starting on left/top, so the inline
   style.left = '0' / style.top = '0' writes take effect immediately
   and getBoundingClientRect() reports the correct origin.

2. index.tsx: Remove the redundant onAlign() call in
   onVisibleChanged. The initial alignment is already performed via the
   motionPrepareResolve useLayoutEffect path; the second call causes
   the oscillation described above.

## Verification

- tsc --noEmit passes with no errors.
- Reproduced and verified the fix against a production app using
  antd@6.3.1 (depends on @rc-component/trigger@3.9.0) + React 19.2:
  - Before fix: popup at top: -7343px (invisible) on every open.
  - After fix: popup at top: 41px (trigger.bottom + offset), visible
    and interactive.

Fixes react-component#618
@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the afc163's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ac011829-3976-4235-927f-e19ef234b57e

📥 Commits

Reviewing files that changed from the base of the PR and between a2fcd03 and c28e7e8.

📒 Files selected for processing (2)
  • src/hooks/useAlign.ts
  • src/index.tsx

Walkthrough

本次变更调整弹层对齐测量期间的过渡状态,并移除可见性变化回调中的重复对齐调用;原有对齐计算、偏移、翻转及可见性回调顺序保持不变。

Changes

弹层对齐流程

Layer / File(s) Summary
测量期间禁用并恢复过渡
src/hooks/useAlign.ts
onAlign 在尺寸读取前保存并禁用 transition,完成布局样式恢复后还原原始值。
可见性变更跳过重复对齐
src/index.tsx
onVisibleChanged 移除 onAlign() 调用,并继续按原顺序触发可见性变更回调。

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: yoyo837, zombiej

Poem

小兔挥耳测尺寸,
暂停过渡读得真。
可见切换不重算,
对齐稳稳落原准。
跳呀跳,代码新!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确概括了通过调整 CSSTransition 规避测量干扰导致弹层不可见的主要改动。
Linked Issues check ✅ Passed 实现了 #618 的核心要求:测量前临时禁用 transition 并在之后恢复,同时移除了多余的二次 onAlign。
Out of Scope Changes check ✅ Passed 未见与既定目标无关的额外代码变更,修改都围绕对齐测量与可见性回调展开。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request temporarily disables CSS transitions on the popup element during alignment measurements to prevent layout calculation bugs, and removes a redundant onAlign call that caused oscillating offsets. The review feedback recommends using setProperty with 'important' to guarantee that transitions are disabled even when stylesheets use !important rules, and properly restoring or removing the property afterward.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/hooks/useAlign.ts
Comment on lines +216 to +217
const originTransition = popupElement.style.transition;
popupElement.style.transition = 'none';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If a stylesheet applies a transition with !important (for example, @media (prefers-reduced-motion: reduce) { * { transition-duration: 0.01ms !important; } } or a custom transition rule with !important), a normal inline style assignment like style.transition = 'none' will not override it because stylesheet !important declarations take precedence over normal inline styles.

To guarantee that the transition is disabled regardless of stylesheet !important declarations, we should use style.setProperty('transition', 'none', 'important') and capture the original property value and priority to restore them correctly.

Suggested change
const originTransition = popupElement.style.transition;
popupElement.style.transition = 'none';
const originTransition = popupElement.style.getPropertyValue('transition') || popupElement.style.transition;
const originPriority = popupElement.style.getPropertyPriority('transition');
popupElement.style.setProperty('transition', 'none', 'important');

Comment thread src/hooks/useAlign.ts
popupElement.style.overflow = originOverflow;
popupElement.style.overflowX = originOverflowX;
popupElement.style.overflowY = originOverflowY;
popupElement.style.transition = originTransition;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Restore the original transition style using setProperty with the captured priority, or remove the property if it was not originally set. This ensures that any original inline transition styles (including those with !important) are correctly restored, or completely cleaned up if there were none.

Suggested change
popupElement.style.transition = originTransition;
if (originTransition) {
popupElement.style.setProperty('transition', originTransition, originPriority);
} else {
popupElement.style.removeProperty('transition');
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useAlign.js: inline style reset to 0 is silently overridden under prefers-reduced-motion CSS (CSSTransition interpolation)

1 participant