Skip to content

feat(ui): Origin-aware open/close animations for popover menus and selects#9113

Open
maxyinger wants to merge 8 commits into
mainfrom
max/popover-enter-exit-anim
Open

feat(ui): Origin-aware open/close animations for popover menus and selects#9113
maxyinger wants to merge 8 commits into
mainfrom
max/popover-enter-exit-anim

Conversation

@maxyinger

@maxyinger maxyinger commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Description

Improves snappyness and adds origin awareness of org switcher, user button, menus & select popover anims.

Before After
https://github.com/user-attachments/assets/7c236e2e-ad89-48e6-80c8-0eca8ef3b11e https://github.com/user-attachments/assets/cfaa57ef-e3bd-4a84-9803-2b8b4a8b2cf6

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Summary by CodeRabbit

  • New Features
    • Popover-based surfaces (organization switching, user actions, overflow menus, and selects) now open/close with origin-aware scale-and-fade transitions (entrance plus exit), with softer exits for non-select menus and faster exits for selects.
    • Select dropdowns (including the Phone Input country picker) now support initialScale (with component-specific defaults) and improved exit timing via exitFast.
    • Added new transition timing options: faster duration and swiftOut easing.
  • Bug Fixes
    • Motion effects are disabled when reduced motion is enabled or when appearance animations are turned off.

@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jul 9, 2026 9:44pm
swingset Ready Ready Preview, Comment Jul 9, 2026 9:44pm

Request Review

@changeset-bot

changeset-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ab7d076

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@clerk/ui Patch
@clerk/chrome-extension Patch
@clerk/swingset Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Popover surfaces now animate on open and close with motion-aware timing. Menus, selects, and dropdown consumers pass the new animation props through their popover wrappers, and related tests now wait for exit unmounting.

Changes

Popover exit animation

Layer / File(s) Summary
Transition tokens
packages/ui/src/foundations/transitions.ts
Adds faster duration and swiftOut timing values.
Popover exit transitions
packages/ui/src/elements/Popover.tsx
Adds animateExit, exitFast, and initialScale, computes transform origin, gates motion, and renders mounted exit content in portal and non-portal paths.
Consumer wiring and surface updates
packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx, packages/ui/src/components/OrganizationSwitcher/index.tsx, packages/ui/src/components/UserButton/UserButtonPopover.tsx, packages/ui/src/components/UserButton/index.tsx, packages/ui/src/elements/Menu.tsx, packages/ui/src/elements/Select.tsx, packages/ui/src/elements/PhoneInput/index.tsx, .changeset/popover-enter-exit-animations.md, packages/ui/src/elements/__tests__/Menu.test.tsx
Updates popover consumers to pass animateExit, disables entry animation in OrganizationSwitcher and UserButton, moves menu/select animation styling into Popover props, sets PhoneInput select scaling, documents the change, and updates menu tests for async unmounting.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: LauraBeatris, wobsoriano, iagodahlem

Poem

I thump as popovers softly glide,
Then fade on exit with bunny pride.
New timings hop, new curves take flight,
Reduced motion keeps things right.
🐰✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main UI change: origin-aware open/close animations for popover menus and selects.

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

@pkg-pr-new

pkg-pr-new Bot commented Jul 9, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@9113

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@9113

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@9113

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@9113

@clerk/electron

npm i https://pkg.pr.new/@clerk/electron@9113

@clerk/electron-passkeys

npm i https://pkg.pr.new/@clerk/electron-passkeys@9113

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@9113

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@9113

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@9113

@clerk/express

npm i https://pkg.pr.new/@clerk/express@9113

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@9113

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@9113

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@9113

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@9113

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@9113

@clerk/react

npm i https://pkg.pr.new/@clerk/react@9113

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@9113

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@9113

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@9113

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@9113

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@9113

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@9113

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@9113

commit: ab7d076

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-07-09T21:45:46.054Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 0
🔴 Breaking changes 0
🟡 Non-breaking changes 0
🟢 Additions 0

No API Changes Detected

All packages have stable APIs with no detected changes.


Report generated by Break Check

Last ran on ab7d076.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/ui/src/elements/Popover.tsx (2)

60-72: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hardcoded 200 duplicates the theme token instead of referencing it.

The comment claims close "matches the theme's $slow (200ms) transition duration" but hardcodes the literal instead of referencing that token. Drawer.tsx uses a shared transitionDurationValues.drawer constant for the same purpose, avoiding drift if the theme value changes.

♻️ Proposed fix
-    duration: { open: 0, close: animateExit && isMotionSafe ? 200 : 0 },
+    duration: { open: 0, close: animateExit && isMotionSafe ? transitionDurationValues.slow : 0 },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/elements/Popover.tsx` around lines 60 - 72, The Popover
transition duration is hardcoded to 200 instead of using the shared
theme-derived value, which can drift from the actual token. Update the
`useTransitionStyles` config in `Popover` to reference the same shared
transition-duration constant/pattern used by `Drawer.tsx` (for the close
duration) so the timing stays aligned with the theme token.

76-81: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unsafe generic assertion on isValidElement.

React.isValidElement<{ style?: React.CSSProperties }>(children) only confirms children is a valid React element — it doesn't verify the element actually accepts a style prop. If a non-DOM component (e.g. a component without a passthrough style) is ever passed as children with animateExit, the cloned prop would be silently dropped/ignored by that component. Currently safe in practice since consumers only pass DOM-like elements, but worth a comment noting the assumption.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/elements/Popover.tsx` around lines 76 - 81, The generic on
React.isValidElement in Popover.tsx is only assuming the child accepts a style
prop, not verifying it. Update the content/cloneElement branch in Popover to add
a clear comment near the React.isValidElement/React.cloneElement logic
explaining that animateExit is only intended for DOM-like children that forward
style, and that non-DOM components may ignore the injected styles.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/ui/src/elements/Popover.tsx`:
- Around line 105-112: The non-portal branch in Popover is dropping the
outsideElementsInert prop, so it never reaches FloatingFocusManager when portal
is false. Update the Popover rendering path that uses FloatingFocusManager to
pass outsideElementsInert through there as well, matching the portal-based
branch so the prop works consistently in both cases.

---

Nitpick comments:
In `@packages/ui/src/elements/Popover.tsx`:
- Around line 60-72: The Popover transition duration is hardcoded to 200 instead
of using the shared theme-derived value, which can drift from the actual token.
Update the `useTransitionStyles` config in `Popover` to reference the same
shared transition-duration constant/pattern used by `Drawer.tsx` (for the close
duration) so the timing stays aligned with the theme token.
- Around line 76-81: The generic on React.isValidElement in Popover.tsx is only
assuming the child accepts a style prop, not verifying it. Update the
content/cloneElement branch in Popover to add a clear comment near the
React.isValidElement/React.cloneElement logic explaining that animateExit is
only intended for DOM-like children that forward style, and that non-DOM
components may ignore the injected styles.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 9439f1f7-4c7a-4777-8c69-277d23109fda

📥 Commits

Reviewing files that changed from the base of the PR and between 08ba540 and 789c9ac.

📒 Files selected for processing (6)
  • .changeset/popover-enter-exit-animations.md
  • packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx
  • packages/ui/src/components/OrganizationSwitcher/index.tsx
  • packages/ui/src/components/UserButton/UserButtonPopover.tsx
  • packages/ui/src/components/UserButton/index.tsx
  • packages/ui/src/elements/Popover.tsx

Comment thread packages/ui/src/elements/Popover.tsx
@maxyinger maxyinger force-pushed the max/popover-enter-exit-anim branch from 789c9ac to 896c8d7 Compare July 9, 2026 20:06
@maxyinger maxyinger changed the title fix(ui): Rework OrganizationSwitcher and UserButton popover animations feat(ui): Origin-aware open/close animations for popover menus and selects Jul 9, 2026
@maxyinger maxyinger force-pushed the max/popover-enter-exit-anim branch from 896c8d7 to 3e29ac4 Compare July 9, 2026 20:11

@coderabbitai coderabbitai 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.

♻️ Duplicate comments (1)
packages/ui/src/elements/Popover.tsx (1)

152-165: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

outsideElementsInert still not forwarded on the non-portal path.

This branch was touched again in this PR but still omits outsideElementsInert on FloatingFocusManager, unlike the portal branch (line 140). This was already flagged in a previous review round and remains unresolved.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/elements/Popover.tsx` around lines 152 - 165, The non-portal
render path in Popover still drops outsideElementsInert on FloatingFocusManager,
unlike the portal branch. Update the FloatingFocusManager usage in the Popover
component’s shouldRender branch to forward outsideElementsInert from the same
props/state source used in the portal path, keeping the two branches consistent.
🧹 Nitpick comments (1)
packages/ui/src/elements/Popover.tsx (1)

125-130: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Silent no-op animation for non-single-element children.

content only applies transitionStyles when children is a single valid element; for arrays/fragments/text it silently falls back to children with no animation and no warning. Currently all call sites pass a single element, so this is low risk today, but it's a fragile contract for future consumers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/elements/Popover.tsx` around lines 125 - 130, The animation
logic in Popover’s content handling only works for a single valid React element,
so non-element children silently skip `transitionStyles` and lose exit
animation. Update the `content`/`React.cloneElement` path to either enforce or
normalize a single animatable child in `Popover` (or document and guard the
contract) so future callers don’t get a no-op when passing fragments, arrays, or
text.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@packages/ui/src/elements/Popover.tsx`:
- Around line 152-165: The non-portal render path in Popover still drops
outsideElementsInert on FloatingFocusManager, unlike the portal branch. Update
the FloatingFocusManager usage in the Popover component’s shouldRender branch to
forward outsideElementsInert from the same props/state source used in the portal
path, keeping the two branches consistent.

---

Nitpick comments:
In `@packages/ui/src/elements/Popover.tsx`:
- Around line 125-130: The animation logic in Popover’s content handling only
works for a single valid React element, so non-element children silently skip
`transitionStyles` and lose exit animation. Update the
`content`/`React.cloneElement` path to either enforce or normalize a single
animatable child in `Popover` (or document and guard the contract) so future
callers don’t get a no-op when passing fragments, arrays, or text.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 2d5e46c9-c7de-47f7-b21d-8b2787ff307f

📥 Commits

Reviewing files that changed from the base of the PR and between 789c9ac and 896c8d7.

📒 Files selected for processing (10)
  • .changeset/popover-enter-exit-animations.md
  • packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx
  • packages/ui/src/components/OrganizationSwitcher/index.tsx
  • packages/ui/src/components/UserButton/UserButtonPopover.tsx
  • packages/ui/src/components/UserButton/index.tsx
  • packages/ui/src/elements/Menu.tsx
  • packages/ui/src/elements/PhoneInput/index.tsx
  • packages/ui/src/elements/Popover.tsx
  • packages/ui/src/elements/Select.tsx
  • packages/ui/src/foundations/transitions.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/popover-enter-exit-animations.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/ui/src/components/OrganizationSwitcher/index.tsx
  • packages/ui/src/components/UserButton/index.tsx
  • packages/ui/src/components/UserButton/UserButtonPopover.tsx
  • packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant