Repository navigation
Refine the admin menu view transition animation - #11912
westonruter wants to merge 22 commits into
Conversation
Clip each top-level menu item's view transition snapshot to its animating group box. By default a captured snapshot keeps its full intrinsic height, so a newly expanded submenu painted at full height from the first frame and overlapped the menu items below while they were still sliding into place. Setting `overflow: clip` on the image pair makes the submenu wipe open, and closed in reverse, in step with those items. Also name the collapse button so it slides into place with the menu items rather than cross-fading via the page root, and add comments explaining each rule. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. Props Bot commandsYou can trigger Props Bot actions by commenting on this PR:
|
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
|
In the following videos, I have a plugin active which slows down the animations to make them more observable: add_action(
'admin_print_styles',
function () {
?>
<style>
/* TEMP DEBUG: slow every view transition animation down to 2s. Remove before shipping. */
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation-duration: 1s;
}
</style>
<?php
}
);Before:Screen.Recording.2026-05-20.at.16.26.37.movAfter:Screen.Recording.2026-05-20.at.16.28.04.mov |
A cross-document view transition snapshots the incoming page at its first render, which can occur before the body has finished parsing. When that happens the admin menu is only partially built in the snapshot, so the menu-top elements have no height delta and the section accordion silently degrades to a plain cross-fade. Print a render-blocking `<link rel="expect">` pointed at the end of the body, so the first render — and therefore the transition's snapshot — waits until the page is fully parsed. The media query scopes the small first-render delay to when view transitions run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diagnosing the intermittent accordion failureSymptom. Navigating between admin sections, the menu "accordion" (one section's submenu collapsing as the next expands) usually played, but intermittently degraded to a plain cross-fade with no movement. It was hard to reproduce — and, crucially, it happened more often with DevTools closed. Ruled out along the way:
Instrumentation. A temporary Root cause. Every captured failure showed the same two things: A cross-document view transition snapshots the incoming page at its first render, and the browser paints incrementally — so first paint (and therefore the snapshot) can happen before the Fix. A render-blocking expectation in the admin <link rel="expect" href="#wpfooter" blocking="render" media="(prefers-reduced-motion: no-preference)">This holds the first render — and so the view transition's snapshot — until The two CSS refinements in the first commit — clipping each menu item's snapshot so a submenu wipes open/closed instead of overlapping the items below, and naming |
…hould return string but returns string|false.
There was a problem hiding this comment.
Pull request overview
This PR refines wp-admin view-transition behavior so the admin menu accordion animation is captured after the page is fully parsed, clips submenu snapshots during expansion/collapse, and includes the collapse button in the menu transition.
Changes:
- Adds a render-blocking
rel="expect"link in admin head for complete-page transition snapshots. - Adds CSS transition grouping/clipping for top-level admin menu items.
- Names the collapse menu item for independent view-transition animation.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/wp-includes/view-transitions.php |
Adds the render-blocking link printer and hardens CSS file reading to return a string. |
src/wp-includes/default-filters.php |
Hooks the new render-blocking link output into admin_head. |
src/wp-admin/css/view-transitions.css |
Adds menu item transition classes, collapse button naming, and snapshot clipping. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@mukeshpanchal27 How's this look to you? |
…sition-improvements * origin/trunk: (53 commits) REST API: Allow-list ability schema response keywords Plugins: Fix plugin card width calculation on the Add Plugins screen. Build/Test Tools: Remove unused reusable workflow. Editor: add responsive global styles for blocks. Build/Test Tools: Configure Composer caching with input where possible. Twenty Eleven: Add missing documentation for some global variables. Abilities API: Add coverage for top-level `required` in input validation Abilities API: Add coverage for ignored schema `validate_callback`/`sanitize_callback` HTML API: Fixes for issues discovered while fuzzing. Twenty Ten: Add missing documentation for some global variables. Privacy: Correct type of `WP_User_Request::$user_id` from `int` to `string`/`numeric-string`. Charset: Add missing `return` statement to `_mb_ord()`. Docs: Add missing @SInCE tags to WP_REST_Icons_Controller. Docs: Add missing @SInCE tags to `WP_Icons_Registry`. KSES: Decode style attribute before sending to safecss_filter_attr(). Twenty Sixteen: Correct display of italics in citations. Docs: Correct some typos in JS inline comments and DocBlocks. Docs: Remove duplicate documentation for the `rest_block_hooks_post_types` hook. Docs: Fix typo in `WP_Query` post-loading comment. Media: Re-introduce client-side media processing feature. ...
…link()`. Covers that the function is hooked at `admin_head` and that it prints a single render-blocking `<link rel="expect">` tag with the expected attributes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mukeshpanchal27
left a comment
There was a problem hiding this comment.
Thanks, @westonruter, for working on this. Overall, it looks solid to me.
I did notice one issue: when the menu is collapsed and a menu item is clicked, the menu seems to lose its original collapsed state. Can you check whether you can reproduce that?
Screen recording:
Screen.Recording.2026-06-04.at.5.40.03.PM.mov
|
@mukeshpanchal27 Hummm. I was able to reproduce this once, but then I couldn't reproduce it anymore. Is this reliably reproducible? |
Yes. In Test this pull request with WordPress Playground instance. |
|
@b1ink0 Given your experience with View Transitions, could I get your help to review this and identify/fix anything that is amiss? I'm somewhat stuck. |
I was able to reproduce this. It looks like a Playground-specific cookie sync issue rather than an issue with this change. WordPress stores the collapsed state in the Also, admin View Transitions are currently intentionally disabled in Playground on Chromium due to a browser crash workaround WordPress/wordpress-playground#3886. cc: @westonruter |
|
@b1ink0 Thanks so much for this investigation. So other than this known defect in Playground, this gets your ✅ of approval? I'll move to commit if so. |
| * @link https://html.spec.whatwg.org/multipage/links.html#link-type-expect | ||
| */ | ||
| function wp_print_view_transitions_render_blocking_link(): void { | ||
| echo '<link rel="expect" href="#wpfooter" blocking="render" media="(prefers-reduced-motion: no-preference)">' . "\n"; |
There was a problem hiding this comment.
We could use #adminmenu. I tested it and didn’t see any issues, but could you please check whether you find any problems or think this is too early in parsing to use?
Else everything looks good to me.
| echo '<link rel="expect" href="#wpfooter" blocking="render" media="(prefers-reduced-motion: no-preference)">' . "\n"; | |
| echo '<link rel="expect" href="#adminmenu" blocking="render" media="(prefers-reduced-motion: no-preference)">' . "\n"; |
There was a problem hiding this comment.
Sorry for the delay. You're right that #wpfooter is bad because it would block rendering the whole page. But #adminmenu is too early in the page. In 326d29e a middle-ground was picked: #wpcontent. Since this opens right after the admin menu and before the footer:
|
🤖 Comment by Claude Opus 5.5 A code review of this branch was run, and its findings about the render-blocking Cost of the
|
expect target |
FCP | Added delay |
|---|---|---|
| none | 252 ms | — |
#adminmenu |
252 ms | 0 |
#wpcontent |
252 ms | 0 |
#wpbody |
252 ms | 0 |
#wpfooter (current) |
336 ms | +84 ms |
The local server sends the whole response at once, so these numbers measure parse time only. When a server streams a slow page, #wpfooter would also hold first paint while the server generates everything above the footer, so the real cost may be higher.
Chrome unblocks at the target's start tag
A test page was set up with a target <ul> that contains a parser-blocking script taking 1.5 s:
expect target |
FCP |
|---|---|
The <ul> itself |
44 ms (the script inside it finished at 1524 ms) |
An element after the <ul> |
1540 ms |
| A missing id | 1540 ms (blocked until parsing ended) |
The block is lifted once the target's start tag is parsed, not once its children are. So <ul id="adminmenu"> doesn't guarantee that the menu items or #collapse-menu are parsed at first render. It works in practice only because the menu markup is small enough to be parsed in one pass. <div id="wpcontent"> opens right after the menu's closing </ul></div></div> in admin-header.php, so its start tag means the whole menu is parsed, at the same cost as #adminmenu.
Other confirmed findings
-
The link is also printed on screens that never output the target.
admin_headfires fromiframe_header()andwp_iframe()too. On those screens the target never appears, so first paint is blocked until parsing ends, footer scripts included:media-upload.php: 216 → 500 ms (+284 ms)- plugin-information modal: 516 → 776 ms (+260 ms)
These screens have no
#adminmenuor#wpcontenteither, so changing the target doesn't fix this. An early return whenIFRAME_REQUESTis defined would cover most of them (media-upload.php,update.php, plugin-information), but not everyiframe_header()caller. The single-site theme-information screen, for example, doesn't appear to define it. The font-library and options-connectors screens do output#wpfooterand aren't affected. -
The link doesn't depend on the view-transitions style being enqueued. If a plugin dequeues
wp-view-transitions-adminor removeswp_enqueue_view_transitions_admin_css, every admin screen still delays first paint, with no transition to show for it.admin_enqueue_scriptsruns beforeadmin_head, so awp_style_is( 'wp-view-transitions-admin' )check would keep the two in sync.
Proposed changes
- Change the target to
href="#wpcontent", and update the docblock to say the target is the element right after the admin menu. - Return early when the style isn't enqueued or
IFRAME_REQUESTis defined. - Add tests for both early returns.
Not yet verified
These review findings haven't been checked yet:
- The
(prefers-reduced-motion: no-preference)media query is duplicated in PHP and CSS. overflow: clipon::view-transition-image-pair(.wp-menu-top)may cut off the current-item arrow or focus rings during the transition.- The
collapse-menutransition name could clash with a plugin menu-top whose id is alsocollapse-menu. A prefixed name likewp-collapse-menuwould avoid that. - The
(string) file_get_contents()cast turns a missing CSS file into an empty style, while the render-blocking link is still printed.
Waiting for `#wpfooter` held the first render until all of the page content had been parsed, which on an `edit.php` list table with 109 posts delayed FCP by about 84 ms at 4x CPU throttling, and more when the server streams a slow page. Chrome unblocks rendering once the target's start tag is parsed, not its children, so `#adminmenu` would not guarantee that the menu items are parsed. `#wpcontent` opens right after the menu closes, so its start tag means the whole menu has been parsed, and it adds no measurable delay compared with having no expect link at all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The link was printed on every admin screen regardless of whether the `wp-view-transitions-admin` style was enqueued. When a plugin dequeues the style or unhooks `wp_enqueue_view_transitions_admin_css()`, the first render was still delayed with no transition to show for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`admin_head` also fires from `iframe_header()` and `wp_iframe()`, whose screens have no admin menu. An expect link whose target never appears blocks the first render until the whole document has been parsed, footer scripts included, which delayed FCP by about 280 ms on `media-upload.php` and 260 ms on the plugin-information modal at 4x CPU throttling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hooking `wp_enqueue_view_transitions_admin_css()` to `admin_enqueue_scripts` also enqueued the style on iframe screens, since `iframe_header()` and `wp_iframe()` fire that action too, even though those screens have no admin menu to animate. Enqueuing it from admin-header.php, next to `colors`, makes it opt-in for the screens that print the menu, including iframe screens that do not define `IFRAME_REQUEST`, such as the theme-information modal. To disable admin View Transitions, a plugin should now dequeue the `wp-view-transitions-admin` style rather than unhook the function. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Checking that the `wp-view-transitions-admin` style is `done` rather than merely enqueued ties the link to the CSS actually having been output in the head. If the style is enqueued too late, it is printed in the footer, and there is no transition at first render to wait for. Since the style is now only enqueued from admin-header.php, this check also leaves out iframe screens, so the `IFRAME_REQUEST` check is removed. It missed iframe screens that do not define that constant, such as the theme-information modal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The docblock said the media query limits the first-render delay to when view transitions actually run, but `prefers-reduced-motion` only reflects a motion preference. A reload or a direct visit is also held until `#wpcontent` is parsed, which is negligible since that element starts right after the admin menu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… expect link. iframe_header() fires the same enqueue and head actions as admin-header.php, so this guards against the enqueue being hooked to one of those actions again, which would print an expect link whose target never appears on iframe screens. The test fails when the `admin_enqueue_scripts` hook is restored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
🤖 Comment by Claude Opus 5.5 Follow-up to the earlier comment. Its proposals have been pushed, except that the What changed
Verified in the browser: Compatibility: how to opt out now
add_action(
'admin_enqueue_scripts',
static function () {
wp_dequeue_style( 'wp-view-transitions-admin' );
},
11
);That was verified locally to remove both the style and the Still open
|
The `overflow: clip` on each menu-top's image pair also clipped a flyout submenu that was open on hover or focus when navigating, since the flyout extends outside its menu-top box. Clicking a flyout link, a common way to navigate the admin, made the flyout vanish on the first frame of the transition. Give an open flyout of a non-current item its own view transition name, so it is captured outside the clipped menu-top and fades out. The current item's submenu is in the normal flow and stays with its menu-top. `match-element` generates a unique name for each flyout, so several open at once cannot collide and abort the transition. Browsers without `match-element` ignore the declaration and keep the clipped behavior. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the menu is folded, either explicitly or automatically between 783 and 960 pixels wide, every submenu is a flyout, including the current item's, so its flyout was still cut off by the menu-top clip. Name those flyouts too. Limit all of the flyout rules to 783 pixels and wider. Below that the menu is responsive and its submenus are in the normal flow, so naming one would pull it out of its clipped menu-top. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
🤖 Comment by Claude Opus 5.5 The flyout issue listed as still open in the previous comment was confirmed and is now fixed. The problemTo test it, a transition was frozen at 40 ms of its 250 ms by pausing the view transition animations from a The fix
TestingTested in Chrome 156, in both LTR and forced RTL, by freezing transitions and checking the old page's state in
The view-transitions PHPUnit group passes on single site and multisite. The only item still open is reading the CSS file on every request, which is being left for a follow-up. |
Giving an open flyout its own view transition name kept it from being clipped, but as an independent group that exists only on the outgoing page it faded out in place, staying behind while its menu item moved to its new position. Before the flyout was named, it was part of its menu item's snapshot and moved along with it. Nest the flyout's group inside its menu item's group with `view-transition-group: nearest`, so it moves with the menu item again while fading out. The nested group is a sibling of the menu item's clipped image pair, so it is still not cut off. Browsers without nested view transition groups ignore the declaration and fade it out in place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Here's a side-by-side video showing before and after (props Claude), with the animations slowed down so you can more easily compare. Even so, downloading the video and scrubbing the it back-and-forth at the animation points (e.g. in Quicktime) may be helpful to see what is going on: side-by-side-expanded.mp4Take note especially the animations of the elements below the active menu item when navigating. Before there were strange overlaps going on. After, it just feels more “right”. |
|
🤖 Comment by Claude Opus 5.5 Follow-up to the flyout comment and the side-by-side video. New since the last update: flyouts move with their menu item6eeb4ed adds With ad67e1e, an open flyout got its own
Nested view transition groups shipped in Chrome 140. Browsers without them ignore the declaration, and the flyout fades out in place, as it did before this commit. To verify it, transitions were frozen at 400 ms of a 1 s animation:
Narration for the videoThe left half is trunk and the right half is this PR. Animations are slowed to 1 s. Each navigation is clicked from a hover flyout unless marked "top level", and the flyout opens 0.5 s before each click.
The flyouts in both halves move with their menu item, so that part looks the same. What differs is the area below the menu item that becomes current: on trunk, collapsing and expanding submenus overlap there. How the video was made
A transition can still start a fraction of a second earlier on one side than the other. Each new page appears when its server response arrives, which varies by request: about 190–250 ms after the click on both branches, with no consistent difference between trunk and this PR. Slow server: the
|
|
@b1ink0 Do you still approve of the changes? 😅 |
Yet me check one more time. |
|
Here's another before/after with 20x CPU throttling. This throttling was how I could first best try to reproduce the original defect, and here it shows the clearest improvement in UX:
Notice how before there was a flash of white between navigations and how the menus didn't always animate. After, the flash of white is gone and the menu feels much more fluid. |
The admin View Transitions style is now enqueued from wp-admin/admin-header.php rather than from a hook, so asserting that it is not hooked to `admin_enqueue_scripts` tests something that is never added. The iframe_header() test guarded against the same thing at the cost of a separate process, which accounted for most of the group's runtime. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…into trac-65032-view-transition-improvements
When view transitions were enabled throughout wp-admin in 7.0, each top-level admin menu item was given a stable
view-transition-name, so navigating between sections animates them into place (the "accordion" effect). Several rough edges remained:<body>has finished parsing. The snapshot then captures a half-built admin menu, so the menu items have no height delta and the accordion silently degrades to a plain cross-fade. It reproduces most easily with DevTools closed;pagerevealinstrumentation confirmed the snapshot being taken atreadyState: "loading"with an incomplete menu.#collapse-menu) was not named, so it cross-faded as part of the page root instead of moving with the rest of the menu.admin_enqueue_scripts, whichiframe_header()also fires, so iframe screens such as Thickbox modals loaded it even though they have no admin menu.A side-by-side video of trunk and this PR is in #11912 (comment), with narration and details on how it was made in #11912 (comment).
Changes
Render blocking until the admin menu is parsed
wp_print_view_transitions_render_blocking_link()function, hooked toadmin_head, prints<link rel="expect" href="#wpcontent" blocking="render" media="(prefers-reduced-motion: no-preference)">. The first render, and therefore the transition's snapshot, waits until the admin menu has been parsed, so the accordion runs reliably.#wpcontent, the element right after the admin menu, because rendering is unblocked as soon as the target's start tag is parsed. Targeting an element near the end of the page (such as#wpfooter) would also work, but it would delay the first render by the time it takes to parse all of the page content.mediaquery limits the delay to when view transitions can run, matching the@view-transitionrule.wp-view-transitions-adminstyle has already been printed (wp_style_is( …, 'done' )). Anexpectlink whose target never appears blocks the first render until the whole document is parsed, so it must not be printed on screens without#wpcontent.Opt-in enqueue from the admin header
wp_enqueue_view_transitions_admin_css()is now called fromwp-admin/admin-header.phpinstead of being hooked toadmin_enqueue_scripts. Only screens that print the admin menu opt in, and iframe screens (which useiframe_header()) no longer get the style or theexpectlink.Menu animation (
view-transitions.css)view-transition-class(wp-menu-top) is added to every top-level menu item, andoverflow: clipis set on::view-transition-image-pair(.wp-menu-top). Each menu item's snapshot is clipped to its animating group box, so an expanding submenu wipes open from the link row to full height, and wipes closed in reverse, in step with the items below it instead of overlapping them.#collapse-menugets its ownview-transition-name, so it slides into place with the menu items instead of cross-fading with the page root.view-transition-name: match-element, so they fade out intact, andview-transition-group: nearestnests each one inside its menu item's group so it moves with the item while fading. This covers the expanded menu, the folded menu, and the auto-folded menu at 783–960px. Below 783px the menu is responsive and its submenus are in the normal flow, so none are named there. Browsers without nested view transition groups fade the flyout out in place.Other
wp_get_view_transitions_admin_css()casts the result offile_get_contents()to a string, to match its return type.wp_print_view_transitions_render_blocking_link(): its hook, the printed link, and that nothing is printed when the style is not enqueued or not yet printed.Backward compatibility
In 7.0, a plugin could turn off admin View Transitions by unhooking the enqueue function:
With this PR, that function is no longer hooked to
admin_enqueue_scripts. It is called directly fromwp-admin/admin-header.php, so theremove_action()call does nothing and View Transitions stay on.Instead, a plugin should dequeue the style:
This works in both 7.0 and 7.2:
admin_enqueue_scriptsat the default priority before any plugin loads, so the plugin's callback at the same priority runs after it and dequeues the style.admin-header.phpenqueues the style before it firesadmin_enqueue_scripts, so the dequeue again comes after the enqueue.Nothing else is needed to turn off the
expectlink. It is only printed when thewp-view-transitions-adminstyle has been printed, so dequeuing the style also removes the link. A plugin does not need to unhookwp_print_view_transitions_render_blocking_link.This recommendation is documented in the docblock for
wp_enqueue_view_transitions_admin_css(), and the change should also be mentioned in the 7.2 dev note.Testing instructions
wp-view-transitions-adminstyle and norel="expect"link.Trac ticket: https://core.trac.wordpress.org/ticket/65032
Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 4.7, Claude Opus 5.5
Used for: Authoring the initial code and PR description (Opus 4.7). Code review, the follow-up fixes (the
#wpcontenttarget, the opt-in enqueue, thedoneguard, and flyout handling), unit tests, testing with Chrome DevTools (LTR, RTL, keyboard, folded and auto-folded menus, mobile, reduced motion, and a simulated slow server), the side-by-side video, and updating this description (Opus 5.5). All changes were reviewed and directed by me.This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.
🤖 Generated with Claude Code