Skip to content

Trac 64389: Refine hoisted stylesheet ordering in classic themes#10875

Closed
westonruter wants to merge 33 commits intoWordPress:trunkfrom
westonruter:trac-64389
Closed

Trac 64389: Refine hoisted stylesheet ordering in classic themes#10875
westonruter wants to merge 33 commits intoWordPress:trunkfrom
westonruter:trac-64389

Conversation

@westonruter
Copy link
Copy Markdown
Member

@westonruter westonruter commented Feb 6, 2026

Trac ticket: https://core.trac.wordpress.org/ticket/64389

When a theme has a theme.json, then it will not emit the classic-theme-styles stylesheet. When no other block styles printed either, as in the case of Elementor with the Hello Elementor theme, then the existing logic in wp_hoist_late_printed_styles() does not have a reference point (from printed block styles) to know where it should inject the global-styles. So, this PR makes the hoisting global-styles much more robust by enqueueing a placeholder inline style which then gets replaced during processing of the template enhancement output buffer. This also allows for removing some convoluted if statements to conditionally inject global styles around other styles.

The same goes for where non-core block styles get printed. Previously the logic here would attempt to locate the classic-theme-styles and then insert the block styles after that style. But if the style was not enqueued, then it would have fall back to trying to insert them after elsewhere. This, again, is simplified by printing a placeholder inline style at the point where block styles normally get inserted. This placeholder style in the HEAD is replaced with the styles rendered in the footer.

The placeholder inline styles are simply removed if there are no appropriate styles to hoist there.

How I tested:

  1. Deactivate Gutenberg, because it does not have a patched gutenberg_enqueue_global_styles() to correspond with the modified wp_enqueue_global_styles() in this PR. Or else, have Sync changes from wp_enqueue_global_styles() to Gutenberg override gutenberg#76127 checked out.
  2. Install and activate the Elementor plugin.
  3. Install and activate the Hello Elementor theme.
  4. Override the Elementor hotfix logic by adding plugin that does: add_filter( 'wp_should_output_buffer_template_for_enhancement', '__return_true' );
  5. It seems a change in trunk eliminated the underline from links (probably Remove link underline style from default theme.json gutenberg#74901), so add this plugin code to restore the previous underline to reproduce the bug with the cascade:
add_filter(
	'print_styles_array',
	static function ( $handles ) {
		if ( in_array( 'global-styles', $handles ) ) {
			wp_add_inline_style( 'global-styles', 'a { text-decoration: underline; }' );
		}
		return $handles;
	}
);
  1. Create a blank post with Elementor.
  2. View the post on the frontend.
❌ Before (links are underlined) ✅ After (links are not underlined)
image image
STYLE#global-styles-inline-css is printed at the end of HEAD, far after LINK#hello-elementor-css STYLE#global-styles-inline-css element is printed after STYLE#wp-emoji-styles-inline-css and before LINK#hello-elementor-css.

Fixing Cascade for wp-block-library added inline styles

This PR also fixes (via 48eb4f6) the cascade for when inline styles are added to wp-block-library. When the combined block library is enqueued, theme authors would expect any inline style to appear in the cascade after any core block styles. However, in 6.9 this was no longer the case as the separate block styles would get inserted after any such wp-block-library inline styles. This is fixed now so that the inline styles for wp-block-library get after any separate core block styles.

For example, consider this plugin code:

add_action(
	'enqueue_block_assets',
	static function () {
		// Extra CSS which prevents empty inline style containing placeholder from being removed.
		wp_add_inline_style( 'wp-block-library', '.wp-block-separator{ outline:solid 1px lime; }' );
	}
);

Before 6.9, in a classic theme where combined block library is loaded, this would result in the following styles printed (e.g. in Twenty Twenty-One):

<link rel='stylesheet' id='wp-block-library-css' href='http://localhost:8000/wp-includes/css/dist/block-library/style.css?ver=7.0-beta2-61752-src' media='all' />
<style id="wp-block-library-inline-css">
.wp-block-separator{ outline:solid 1px lime; }
/*# sourceURL=wp-block-library-inline-css */
</style>
<link rel='stylesheet' id='wp-block-library-theme-css' href='http://localhost:8000/wp-includes/css/dist/block-library/theme.css?ver=7.0-beta2-61752-src' media='all' />
<link rel='stylesheet' id='classic-theme-styles-css' href='http://localhost:8000/wp-includes/css/classic-themes.css?ver=7.0-beta2-61752-src' media='all' />

In 6.9, however, when separate block styles are loaded on demand, the lime color style appears before the block style for the Separator block:

<link rel='stylesheet' id='wp-block-library-css' href='http://localhost:8000/wp-includes/css/dist/block-library/common.css?ver=7.0-beta2-61752-src' media='all' />
<style id="wp-block-library-inline-css">
.wp-block-separator{ outline:solid 1px lime; }

/*# sourceURL=wp-block-library-inline-css */
</style>
<link rel='stylesheet' id='wp-block-separator-css' href='http://localhost:8000/wp-includes/blocks/separator/style.css?ver=7.0-beta2-61752-src' media='all' />
<link rel='stylesheet' id='wp-block-separator-theme-css' href='http://localhost:8000/wp-includes/blocks/separator/theme.css?ver=7.0-beta2-61752-src' media='all' />

<link rel='stylesheet' id='classic-theme-styles-css' href='http://localhost:8000/wp-includes/css/classic-themes.css?ver=7.0-beta2-61752-src' media='all' />

With the changes in this PR, the correct order placement in the cascade is better preserved:

<link rel='stylesheet' id='wp-block-library-css' href='http://localhost:8000/wp-includes/css/dist/block-library/common.css?ver=7.0-beta2-61752-src' media='all' />

<link rel='stylesheet' id='wp-block-separator-css' href='http://localhost:8000/wp-includes/blocks/separator/style.css?ver=7.0-beta2-61752-src' media='all' />
<link rel='stylesheet' id='wp-block-separator-theme-css' href='http://localhost:8000/wp-includes/blocks/separator/theme.css?ver=7.0-beta2-61752-src' media='all' />
<style id="wp-block-library-inline-css-extra">
.wp-block-separator{ outline:solid 1px lime; }
/*# sourceURL=wp-block-library-inline-css */
</style>

<link rel='stylesheet' id='classic-theme-styles-css' href='http://localhost:8000/wp-includes/css/classic-themes.css?ver=7.0-beta2-61752-src' media='all' />

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.


Commit Message

Script Loader: Refine hoisted stylesheet ordering in classic themes.

This introduces placeholder inline STYLE tags in the HEAD which are used to accurately locate hoisted styles which were printed in the footer. This improves the robustness of the hoisting logic. A style placeholder had been used previously in the inline style for wp-block-library for placing core block styles, and new placeholders are added for global styles and non-core block styles.

Furthermore, this fixes the cascade for inline styles added to wp-block-library. When separate block styles are not used, these styles would have appeared after the single combined wp-block-library. However, in 6.9 the order changed so that separate block styles would appear after. To preserve the original cascade, this splits the wp-block-library inline style: the first half (likely just the inlined block-library/common.css) appears before the block styles, and the remainder appears in a new STYLE#wp-block-library-inline-css-extra element.

Developed in #10875

Follow-up to r61554, r61174, r61122, r61076, r61008.

Props westonruter, mmorris8, ozgursar, vanonsopensource, xwolf, immeet94, sabernhardt, george9, joezappie, jorbin, sajib1223, joefusco.
See #64099, #64354, #64150, #43258.
Fixes #64389.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Feb 6, 2026

Test using WordPress Playground

The 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

  • The Plugin and Theme Directories cannot be accessed within Playground.
  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@westonruter westonruter marked this pull request as ready for review February 6, 2026 20:03
@github-actions
Copy link
Copy Markdown

github-actions bot commented Feb 6, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props westonruter, joefusco, adamsilverstein, ocean90.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@westonruter

This comment was marked as duplicate.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refines how global-styles is hoisted into the document <head> for classic themes when block assets are loaded on-demand, addressing cases where no block styles (and no classic-theme-styles) provide a reliable insertion point.

Changes:

  • Enqueue a wp-global-styles-placeholder inline style during wp_enqueue_scripts for classic themes with on-demand loading, so wp_hoist_late_printed_styles() can reliably replace it with the footer-printed global-styles in the <head>.
  • Update wp_hoist_late_printed_styles() to detect and replace the new global-styles placeholder bookmark.
  • Expand PHPUnit coverage to include “Elementor-like” scenarios (theme.json present + no block content + dequeued block library), and adjust expected ordering accordingly.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/wp-includes/script-loader.php Adds/enables a global-styles placeholder in classic+on-demand mode and replaces it during hoisting to stabilize <head> ordering.
tests/phpunit/tests/template.php Updates hoisting tests to include content parameterization and new scenarios/expectations around placeholder-driven ordering.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@westonruter
Copy link
Copy Markdown
Member Author

This new placeholder approach may be relevant the hoisted placement of other styles. But I'd really love to get more reproduction steps for any issues people are experiencing.

@westonruter westonruter marked this pull request as draft February 9, 2026 21:27
@westonruter
Copy link
Copy Markdown
Member Author

Given that this has had been tested and code reviewed, I'm intending to proceed with commit later today so we can be sure it is tested in the next pre-release.

Comment on lines -3709 to -3725
// Capture the styles enqueued at the enqueue_block_assets action, so that non-core block styles and global styles can be inserted afterwards during hoisting.
$style_handles_at_enqueue_block_assets = array();
add_action(
'enqueue_block_assets',
static function () use ( &$style_handles_at_enqueue_block_assets ) {
$style_handles_at_enqueue_block_assets = wp_styles()->queue;
},
PHP_INT_MIN
);
add_action(
'enqueue_block_assets',
static function () use ( &$style_handles_at_enqueue_block_assets ) {
$style_handles_at_enqueue_block_assets = array_values( array_diff( wp_styles()->queue, $style_handles_at_enqueue_block_assets ) );
},
PHP_INT_MAX
);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This code is obsolete as it was just used to set the first_style_at_enqueue_block_assets and last_style_at_enqueue_block_assets bookmarks, which are also now no longer used.

Comment on lines -3886 to -3906
} elseif ( ( 'STYLE' === $processor->get_tag() || 'LINK' === $processor->get_tag() ) && $processor->get_attribute( 'id' ) ) {
$id = $processor->get_attribute( 'id' );
$handle = null;
if ( 'STYLE' === $processor->get_tag() ) {
if ( preg_match( '/^(.+)-inline-css$/', $id, $matches ) ) {
$handle = $matches[1];
}
} elseif ( preg_match( '/^(.+)-css$/', $id, $matches ) ) {
$handle = $matches[1];
}

if ( 'classic-theme-styles' === $handle ) {
$processor->set_bookmark( 'classic_theme_styles' );
}

if ( $handle && in_array( $handle, $style_handles_at_enqueue_block_assets, true ) ) {
if ( ! $processor->has_bookmark( 'first_style_at_enqueue_block_assets' ) ) {
$processor->set_bookmark( 'first_style_at_enqueue_block_assets' );
}
$processor->set_bookmark( 'last_style_at_enqueue_block_assets' );
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This condition is obsolete because the first_style_at_enqueue_block_assets and last_style_at_enqueue_block_assets bookmarks are no longer used.

@westonruter
Copy link
Copy Markdown
Member Author

When prepping this for commit, I sent it through Gemini again for a review and it pointed out that the first_style_at_enqueue_block_assets and last_style_at_enqueue_block_assets bookmarks are no longer used and so they can be removed. In commit dc609a3 the function is simplified significantly.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@ocean90
Copy link
Copy Markdown
Member

ocean90 commented Mar 11, 2026

Just want to confirm that this PR fixes a case in a client project with a classic theme where theme's critical CSS was printed before the global-styles-inline-css style tag.

if ( $is_classic_theme && doing_action( 'wp_enqueue_scripts' ) && $assets_on_demand ) {
if ( has_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' ) ) {
wp_register_style( 'wp-global-styles-placeholder', false );
wp_add_inline_style( 'wp-global-styles-placeholder', ':root { --wp-internal-comment: "Placeholder for wp_hoist_late_printed_styles() to replace with the global-styles printed at wp_footer." }' );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thats cure, I've never seen style comments added like this.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Just a CSS custom property (variable) leveraged as a comment 😄

Copy link
Copy Markdown
Member

@adamsilverstein adamsilverstein left a comment

Choose a reason for hiding this comment

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

Tremendous! Left some nit pick feedback about comments.

westonruter added a commit to WordPress/gutenberg that referenced this pull request Mar 11, 2026
pento pushed a commit that referenced this pull request Mar 11, 2026
…l CSS cascade in classic themes.

This introduces placeholder inline `STYLE` tags in the `HEAD` which are used to accurately locate hoisted styles which were printed in the footer. This improves the robustness of the hoisting logic. A style placeholder had been used previously in the inline style for `wp-block-library` for placing core block styles, and new placeholders are added for global styles and non-core block styles.

Furthermore, this fixes the cascade for inline styles added to `wp-block-library`. When separate block styles are not used, these styles would have appeared after the single combined `wp-block-library`. However, in 6.9 the order changed so that separate block styles would appear after. To preserve the original cascade, this splits the `wp-block-library` inline style: the first half (likely just the inlined `block-library/common.css`) appears before the block styles, and the remainder appears in a new `STYLE#wp-block-library-inline-css-extra` element.

Developed in #10875

Follow-up to r61554, r61174, r61122, r61076, r61008.

Props westonruter, joefusco, adamsilverstein, ocean90, mmorris8, ozgursar, vanonsopensource, xwolf, immeet94, george9, joezappie, jorbin, sajib1223, sabernhardt.
See #64099, #64354, #64150, #43258.
Fixes #64389.


git-svn-id: https://develop.svn.wordpress.org/trunk@61945 602fd350-edb4-49c9-b593-d223f7449a82
markjaquith pushed a commit to markjaquith/WordPress that referenced this pull request Mar 11, 2026
…l CSS cascade in classic themes.

This introduces placeholder inline `STYLE` tags in the `HEAD` which are used to accurately locate hoisted styles which were printed in the footer. This improves the robustness of the hoisting logic. A style placeholder had been used previously in the inline style for `wp-block-library` for placing core block styles, and new placeholders are added for global styles and non-core block styles.

Furthermore, this fixes the cascade for inline styles added to `wp-block-library`. When separate block styles are not used, these styles would have appeared after the single combined `wp-block-library`. However, in 6.9 the order changed so that separate block styles would appear after. To preserve the original cascade, this splits the `wp-block-library` inline style: the first half (likely just the inlined `block-library/common.css`) appears before the block styles, and the remainder appears in a new `STYLE#wp-block-library-inline-css-extra` element.

Developed in WordPress/wordpress-develop#10875

Follow-up to r61554, r61174, r61122, r61076, r61008.

Props westonruter, joefusco, adamsilverstein, ocean90, mmorris8, ozgursar, vanonsopensource, xwolf, immeet94, george9, joezappie, jorbin, sajib1223, sabernhardt.
See #64099, #64354, #64150, #43258.
Fixes #64389.

Built from https://develop.svn.wordpress.org/trunk@61945


git-svn-id: http://core.svn.wordpress.org/trunk@61227 1a063a9b-81f0-0310-95a4-ce76da25c4cd
@github-actions
Copy link
Copy Markdown

A commit was made that fixes the Trac ticket referenced in the description of this pull request.

SVN changeset: 61945
GitHub commit: 2016709

This PR will be closed, but please confirm the accuracy of this and reopen if there is more work to be done.

@github-actions github-actions bot closed this Mar 11, 2026
westonruter added a commit to WordPress/gutenberg that referenced this pull request Mar 11, 2026
…76127)

* Sync changes from wp_enqueue_global_styles() to gutenberg override

* Sync latest changes from WordPress/wordpress-develop#10875

Co-authored-by: westonruter <[email protected]>
Co-authored-by: adamsilverstein <[email protected]>
gutenbergplugin pushed a commit to WordPress/gutenberg that referenced this pull request Mar 11, 2026
…76127)

* Sync changes from wp_enqueue_global_styles() to gutenberg override

* Sync latest changes from WordPress/wordpress-develop#10875

Co-authored-by: westonruter <[email protected]>
Co-authored-by: adamsilverstein <[email protected]>
peterwilsoncc pushed a commit to peterwilsoncc/gutenberg-build that referenced this pull request Mar 12, 2026
…(#76127)

* Sync changes from wp_enqueue_global_styles() to gutenberg override

* Sync latest changes from WordPress/wordpress-develop#10875

Co-authored-by: westonruter <[email protected]>
Co-authored-by: adamsilverstein <[email protected]>

Source: WordPress/gutenberg@01ebc90
peterwilsoncc pushed a commit to peterwilsoncc/gutenberg-build that referenced this pull request Mar 12, 2026
…(#76127)

* Sync changes from wp_enqueue_global_styles() to gutenberg override

* Sync latest changes from WordPress/wordpress-develop#10875

Co-authored-by: westonruter <[email protected]>
Co-authored-by: adamsilverstein <[email protected]>

Source: WordPress/gutenberg@9cc64f5
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.

5 participants