Commit 6399f6f461 for wordpress.org
commit 6399f6f461f4997c4e1c2dbead650c43d43baa6e
Author: westonruter <westonruter@git.wordpress.org>
Date: Fri Oct 23 22:25:54 2026 +0000
Administration: Refine the admin menu view transitions.
The admin menu's accordion animation sometimes degraded to a plain cross-fade, because a cross-document view transition snapshots the incoming page at its first render, which can happen before the admin menu has finished parsing. A render-blocking `expect` link now holds the first render until `#wpcontent`, the element right after the admin menu, is parsed, so the snapshot always has the complete menu without waiting for the rest of the page. The link is only printed once the View Transitions style has been printed, since an `expect` target that never appears blocks rendering until the whole document is parsed.
The animation itself is also refined. Each menu item's snapshot is clipped to its animating box, so a submenu wipes open or closed in step with the items around it instead of overlapping them. The Collapse Menu button moves with the menu instead of cross-fading. An open flyout fades out while moving with its menu item, instead of being cut off by that clip.
The style is now enqueued from `wp-admin/admin-header.php` instead of on the `admin_enqueue_scripts` action, which `iframe_header()` also fires, so screens without the admin menu no longer load it. As a result, unhooking `wp_enqueue_view_transitions_admin_css()` no longer turns off admin View Transitions. Dequeuing the `wp-view-transitions-admin` style does, and it works in 7.0 as well.
Developed in https://github.com/WordPress/wordpress-develop/pull/11912.
Follow-up to r61491.
Props westonruter, b1ink0, zieladam, mukesh27, dhrupo, audrasjb, joedolson, huzaifaalmesbah, jasonsa19, ozgursar, nikunj8866, darshitrajyaguru97.
See #64470.
Fixes #65032.
Built from https://develop.svn.wordpress.org/trunk@64691
git-svn-id: http://core.svn.wordpress.org/trunk@63841 1a063a9b-81f0-0310-95a4-ce76da25c4cd
diff --git a/wp-admin/admin-header.php b/wp-admin/admin-header.php
index 055d801533..36962c3e44 100644
--- a/wp-admin/admin-header.php
+++ b/wp-admin/admin-header.php
@@ -103,6 +103,12 @@ wp_enqueue_style( 'colors' );
wp_enqueue_script( 'utils' );
wp_enqueue_script( 'svg-painter' );
+/*
+ * View transitions animate the admin menu, so they are only enqueued on screens that print it.
+ * Iframe screens such as Thickbox modals do not load this header and so do not opt in.
+ */
+wp_enqueue_view_transitions_admin_css();
+
$admin_body_class = preg_replace( '/[^a-z0-9_-]+/i', '-', $hook_suffix );
// Print the global admin inline scripts through the script tag API so the
diff --git a/wp-admin/css/view-transitions.css b/wp-admin/css/view-transitions.css
index 538a90b502..78adf39075 100644
--- a/wp-admin/css/view-transitions.css
+++ b/wp-admin/css/view-transitions.css
@@ -3,7 +3,74 @@
navigation: auto;
}
+ /*
+ * Give each top-level menu item its own view transition name — its id,
+ * such as "menu-posts" — so each one is captured as an independent group
+ * and animates from its old position and size to its new ones across a
+ * navigation. This produces the accordion effect: the previously open
+ * section collapses as the newly current one expands, instead of the
+ * whole menu simply cross-fading. attr() falls back to "none" for any
+ * item whose id is missing or is not a valid custom identifier. The
+ * shared wp-menu-top class lets the image-pair rule below address every
+ * menu-top group at once.
+ */
#adminmenu > .menu-top {
view-transition-name: attr(id type(<custom-ident>), none);
+ view-transition-class: wp-menu-top;
+ }
+
+ /*
+ * Name the collapse button too, so it slides into place with the menu
+ * items instead of cross-fading as part of the page root.
+ */
+ #collapse-menu {
+ view-transition-name: collapse-menu;
+ }
+
+ /*
+ * Clip each menu-top's snapshot to its animating group box. By default a
+ * captured snapshot keeps its full intrinsic height, so the new (expanded)
+ * submenu paints at full height from the first frame and overlaps the
+ * items below while they are still sliding down. Clipping the image pair
+ * to the group — whose height animates from the link row to the full
+ * expanded height — makes the submenu wipe open, and closed in reverse,
+ * in step with those items.
+ */
+ ::view-transition-image-pair(.wp-menu-top) {
+ overflow: clip;
+ }
+
+ /*
+ * Capture an open flyout submenu — one shown on hover or focus — as its own
+ * group, so that it fades out instead of being cut off by the clip above,
+ * since it extends outside its menu-top box. match-element gives each
+ * flyout a unique name, so two open at once cannot collide and abort the
+ * transition. view-transition-group nests the flyout's group inside its
+ * menu-top's group, so it moves along with its menu item while fading out
+ * instead of staying behind at its old position. Since the nested group is
+ * a sibling of the menu-top's image pair, it is not cut off by the clip.
+ *
+ * In the expanded menu, the current item's submenu is in the normal flow,
+ * so it is left to the clipped menu-top. When the menu is folded, either
+ * explicitly or automatically on narrower screens, every submenu is a
+ * flyout, including the current one. Below 783px the menu is responsive and
+ * its submenus are in the normal flow, so none are named there.
+ */
+ @media screen and (min-width: 783px) {
+ #adminmenu > .wp-not-current-submenu.opensub > .wp-submenu,
+ #adminmenu > .wp-not-current-submenu > a.menu-top:focus + .wp-submenu,
+ .folded #adminmenu > .opensub > .wp-submenu,
+ .folded #adminmenu > .menu-top > a.menu-top:focus + .wp-submenu {
+ view-transition-name: match-element;
+ view-transition-group: nearest;
+ }
+ }
+
+ @media screen and (min-width: 783px) and (max-width: 960px) {
+ .auto-fold #adminmenu > .opensub > .wp-submenu,
+ .auto-fold #adminmenu > .menu-top > a.menu-top:focus + .wp-submenu {
+ view-transition-name: match-element;
+ view-transition-group: nearest;
+ }
}
}
diff --git a/wp-admin/css/view-transitions.min.css b/wp-admin/css/view-transitions.min.css
index 165d3ca6ba..ab1f8dbf38 100644
--- a/wp-admin/css/view-transitions.min.css
+++ b/wp-admin/css/view-transitions.min.css
@@ -1,2 +1,2 @@
/*! This file is auto-generated */
-@media (prefers-reduced-motion:no-preference){@view-transition{navigation:auto}#adminmenu>.menu-top{view-transition-name:attr(id type(<custom-ident>),none)}}
\ No newline at end of file
+@media (prefers-reduced-motion:no-preference){@view-transition{navigation:auto}#adminmenu>.menu-top{view-transition-name:attr(id type(<custom-ident>),none);view-transition-class:wp-menu-top}#collapse-menu{view-transition-name:collapse-menu}::view-transition-image-pair(.wp-menu-top){overflow:clip}@media screen and (min-width:783px){#adminmenu>.wp-not-current-submenu.opensub>.wp-submenu,#adminmenu>.wp-not-current-submenu>a.menu-top:focus+.wp-submenu,.folded #adminmenu>.menu-top>a.menu-top:focus+.wp-submenu,.folded #adminmenu>.opensub>.wp-submenu{view-transition-name:match-element;view-transition-group:nearest}}@media screen and (min-width:783px) and (max-width:960px){.auto-fold #adminmenu>.menu-top>a.menu-top:focus+.wp-submenu,.auto-fold #adminmenu>.opensub>.wp-submenu{view-transition-name:match-element;view-transition-group:nearest}}}
\ No newline at end of file
diff --git a/wp-includes/default-filters.php b/wp-includes/default-filters.php
index c229fbfcb8..7f5f9d0fb0 100644
--- a/wp-includes/default-filters.php
+++ b/wp-includes/default-filters.php
@@ -634,7 +634,7 @@ add_action( 'wp_enqueue_scripts', 'wp_enqueue_classic_theme_styles' );
add_action( 'admin_enqueue_scripts', 'wp_localize_jquery_ui_datepicker', 1000 );
add_action( 'admin_enqueue_scripts', 'wp_common_block_scripts_and_styles' );
add_action( 'admin_enqueue_scripts', 'wp_enqueue_command_palette_assets' );
-add_action( 'admin_enqueue_scripts', 'wp_enqueue_view_transitions_admin_css' );
+add_action( 'admin_head', 'wp_print_view_transitions_render_blocking_link' );
add_action( 'enqueue_block_assets', 'wp_enqueue_classic_theme_styles' );
add_action( 'enqueue_block_assets', 'wp_enqueue_registered_block_scripts_and_styles' );
add_action( 'enqueue_block_assets', 'enqueue_block_styles_assets', 30 );
diff --git a/wp-includes/version.php b/wp-includes/version.php
index 0e667fd610..a118126ecd 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
*
* @global string $wp_version
*/
-$wp_version = '7.2-beta1-64690';
+$wp_version = '7.2-beta1-64691';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.
diff --git a/wp-includes/view-transitions.php b/wp-includes/view-transitions.php
index 2d2ad61e0b..a6323098b1 100644
--- a/wp-includes/view-transitions.php
+++ b/wp-includes/view-transitions.php
@@ -10,7 +10,11 @@
/**
* Enqueues View Transitions CSS for the admin.
*
+ * To disable admin View Transitions, dequeue the `wp-view-transitions-admin` style at the
+ * {@see 'admin_enqueue_scripts'} action, rather than unhooking this function.
+ *
* @since 7.0.0
+ * @since 7.2.0 Called from wp-admin/admin-header.php instead of hooked to {@see 'admin_enqueue_scripts'}.
*/
function wp_enqueue_view_transitions_admin_css(): void {
wp_enqueue_style( 'wp-view-transitions-admin' );
@@ -26,5 +30,41 @@ function wp_enqueue_view_transitions_admin_css(): void {
function wp_get_view_transitions_admin_css(): string {
$affix = SCRIPT_DEBUG ? '' : '.min';
$path = ABSPATH . "wp-admin/css/view-transitions{$affix}.css";
- return file_get_contents( $path );
+ return (string) file_get_contents( $path );
+}
+
+/**
+ * Prints a render-blocking expectation so View Transitions capture a fully parsed admin menu.
+ *
+ * A cross-document view transition snapshots the incoming page at its first render, which can
+ * happen before the `<body>` has finished parsing. The snapshot would then capture a partially
+ * built page — for example an incomplete admin menu — and the transition animates to or from
+ * the wrong state.
+ *
+ * The `expect` link blocks the first render until `#wpcontent` is parsed. Rendering is unblocked
+ * as soon as the target's start tag is parsed, not its children, so the target is the element
+ * right after the admin menu rather than the menu itself: once `#wpcontent` starts, the whole
+ * menu has been parsed. Targeting an element near the end of the body would also work, but it
+ * would delay the first render by the time it takes to parse all of the page content. The media
+ * query limits the first-render delay to when view transitions can run, matching the
+ * `@view-transition` rule in view-transitions.css. It does not detect whether a transition is
+ * actually in progress, so a reload or a direct visit is also held until `#wpcontent` is parsed,
+ * but since that element starts right after the admin menu, the delay is negligible.
+ *
+ * Nothing is printed unless the admin View Transitions style has already been printed, since
+ * there is otherwise no transition to wait for. That style is only enqueued from
+ * wp-admin/admin-header.php, so this also leaves out iframe screens such as Thickbox modals,
+ * which have no `#wpcontent`: an `expect` link whose target never appears blocks the first
+ * render until the whole document has been parsed.
+ *
+ * @since 7.2.0
+ *
+ * @link https://html.spec.whatwg.org/multipage/links.html#link-type-expect
+ */
+function wp_print_view_transitions_render_blocking_link(): void {
+ if ( ! wp_style_is( 'wp-view-transitions-admin', 'done' ) ) {
+ return;
+ }
+
+ echo '<link rel="expect" href="#wpcontent" blocking="render" media="(prefers-reduced-motion: no-preference)">' . "\n";
}