Skip to content

fix(ui): Rework OrganizationSwitcher and UserButton popover animations#9113

Open
maxyinger wants to merge 1 commit into
mainfrom
max/popover-enter-exit-anim
Open

fix(ui): Rework OrganizationSwitcher and UserButton popover animations#9113
maxyinger wants to merge 1 commit into
mainfrom
max/popover-enter-exit-anim

Conversation

@maxyinger

@maxyinger maxyinger commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Description

Fast is a feature. this improves snappyness of our org switcher and user button popover anim. since it's a user initiated action and the affected item is right by the mouse, there's no need for an entrance anim. I think all the anim is doing in this case is making the interaction feel slower since there's no need to guide the eye. this also adds an exit animation which feels nice. follows osx interaction patterns and feels right, but don't see it executed on the web much. lets be one of the first to do it right.

Avatar flash bug was fixed in this PR. figured this was best to not lump in with that cus it might be contentious, but it's good stuff.

Before After
https://github.com/user-attachments/assets/7c236e2e-ad89-48e6-80c8-0eca8ef3b11e https://github.com/user-attachments/assets/e23a8108-271d-4d58-9a4e-9d2bb16b1789

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

    • Popovers for organization switching and user actions now close with a fade-out animation instead of a slide-and-scale entrance.
    • Added support for smoother popover exit transitions while keeping the opening motion instant.
  • Bug Fixes

    • Motion is now automatically disabled when reduced-motion settings are enabled or animations are turned off in appearance settings.

@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 12:40am
swingset Ready Ready Preview, Comment Jul 9, 2026 12:40am

Request Review

@changeset-bot

changeset-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 789c9ac

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

📝 Walkthrough

Walkthrough

The Popover component adds an animateExit prop enabling fade-out-on-close transitions gated by reduced-motion and theme animation settings. OrganizationSwitcher and UserButton popovers now disable entry animation unconditionally and pass animateExit to their Popover usage, with a changeset documenting the behavior.

Changes

Popover exit animation

Layer / File(s) Summary
Popover exit transition core logic
packages/ui/src/elements/Popover.tsx
Adds animateExit prop, motion-safe detection via reduced-motion and theme options, transition styles via useTransitionStyles, and updated render gating (shouldRender/content) in both portal and non-portal paths.
Consumer wiring and changeset
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, .changeset/popover-enter-exit-animations.md
Removes __experimental_asStandalone-based entry animation logic, hardcodes shouldEntryAnimate to false, adds animateExit to Popover invocations, and documents the change in a changeset.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Consumer
  participant Popover
  participant useTransitionStyles
  participant usePrefersReducedMotion

  Consumer->>Popover: render with animateExit=true
  Popover->>usePrefersReducedMotion: check reduced motion preference
  Popover->>Popover: compute isMotionSafe (animations enabled and not reduced)
  alt animateExit enabled and isMotionSafe
    Popover->>useTransitionStyles: request isMounted, styles
    Popover->>Popover: derive shouldRender from isMounted
    Popover->>Popover: clone child element merging transition styles
  end
  Popover-->>Consumer: render content gated by shouldRender
Loading

Poem

A hop, a fade, no more slide-scale dance,
The popover vanishes with elegant chance.
Reduced motion? We pause, we respect,
Instant open, soft exit — precisely correct.
🐰 Thump-thump, the animation's set free!

🚥 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 The title clearly summarizes the main UI change: reworking OrganizationSwitcher and UserButton popover animations.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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: 789c9ac

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-07-09T00:43:34.372Z

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 789c9ac.

@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 on lines +105 to +112
{shouldRender && (
<FloatingFocusManager
context={context}
initialFocus={initialFocus}
order={order}
modal={modal}
>
<>{children}</>
<>{content}</>

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files 'packages/ui/src/elements/Popover.tsx' 'packages/ui/src/**' | sed -n '1,120p'
echo '--- search outsideElementsInert ---'
rg -n "outsideElementsInert|FloatingFocusManager" packages/ui/src -S
echo '--- Popover.tsx around relevant lines ---'
nl -ba packages/ui/src/elements/Popover.tsx | sed -n '1,220p'

Repository: clerk/javascript

Length of output: 8979


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('packages/ui/src/elements/Popover.tsx')
print('exists:', p.exists())
if p.exists():
    text = p.read_text()
    for needle in ['outsideElementsInert', 'FloatingFocusManager', 'portal']:
        print(f'-- {needle} --')
        for i, line in enumerate(text.splitlines(), 1):
            if needle in line:
                print(f'{i}: {line}')
PY

Repository: clerk/javascript

Length of output: 862


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,180p' packages/ui/src/elements/Popover.tsx | cat -n

Repository: clerk/javascript

Length of output: 4850


Forward outsideElementsInert in the non-portal branch

portal={false} currently drops outsideElementsInert, so the prop has no effect there. Pass it through to FloatingFocusManager in the non-portal return as well.

🤖 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 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.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant