WordPress 6.9 moved global styles to the footer: why your dequeue stopped working

Since WordPress 6.9, classic themes get global styles at wp_footer and have them hoisted into the head, so the usual dequeue runs too early. Here's the one-line fix.

If you look after a classic WordPress theme, there’s a good chance you have a function like this somewhere in it:

function mytheme_dequeue_block_styles() {
	wp_dequeue_style( 'wp-block-library' );
	wp_dequeue_style( 'classic-theme-styles' );
	wp_dequeue_style( 'global-styles' );
}
add_action( 'wp_enqueue_scripts', 'mytheme_dequeue_block_styles', 100 );

It’s a common performance tweak. If the site uses the Classic Editor, or only a handful of pages have blocks, there’s no reason to send every visitor the block library and the global styles stylesheet. For years it worked.

On a site I maintain, I noticed a <style id="global-styles-inline-css"> block back in the <head> of every page, about 9 KB of CSS custom properties for colour presets, font sizes and spacing the theme never used. The dequeue was still there and still running. It just wasn’t doing anything any more.

What changed in WordPress 6.9

WordPress 6.9 changed how classic themes load block assets. Block styles are now loaded on demand by default: core waits until it knows which blocks are actually on the page, which it only knows after the content has been rendered. That means some styles are enqueued late, at wp_footer, and then hoisted back up into the <head> by an output buffer before the page is sent.

Global styles follow the same route. If you read wp_enqueue_global_styles() in wp-includes/script-loader.php, you’ll find it’s hooked twice:

add_action( 'wp_enqueue_scripts', 'wp_enqueue_global_styles' );
add_action( 'wp_footer', 'wp_enqueue_global_styles', 1 );

For a classic theme with on-demand loading switched on, the wp_enqueue_scripts call no longer enqueues the stylesheet. It registers an empty placeholder called wp-global-styles-placeholder and returns. The real global styles are enqueued at wp_footer, priority 1, and wp_hoist_late_printed_styles() swaps them in where the placeholder was.

So the old dequeue runs at wp_enqueue_scripts, priority 100, and dequeues a handle that hasn’t been enqueued yet. Then wp_footer fires, the global styles are enqueued for real, and they end up in the head as if nothing had happened.

How to spot it on your own site

View the source of any page and search for global-styles-inline-css. If your theme is meant to remove it and it’s still there, you’re probably hitting this. You may also see a tiny style block with a --wp-internal-comment custom property. That’s the placeholder, left behind when nothing replaces it.

It’s worth checking with a cache-busting query string, such as ?nocache=1, so you’re looking at what PHP produces rather than a cached copy from before the update.

The fix

Unhook the footer enqueue itself, and dequeue the placeholder while you’re there:

function mytheme_dequeue_block_styles() {
	// Keep block styles on singular views whose content really has blocks.
	if ( is_singular() && has_blocks( get_queried_object_id() ) ) {
		return;
	}

	wp_dequeue_style( 'wp-block-library' );
	wp_dequeue_style( 'wp-block-library-theme' );
	wp_dequeue_style( 'classic-theme-styles' );
	wp_dequeue_style( 'global-styles' );

	// Since 6.9, classic themes get global styles at wp_footer, hoisted
	// into the head afterwards, so the dequeue above is too early.
	remove_action( 'wp_footer', 'wp_enqueue_global_styles', 1 );
	wp_dequeue_style( 'wp-global-styles-placeholder' );
}
add_action( 'wp_enqueue_scripts', 'mytheme_dequeue_block_styles', 100 );

The remove_action() has to name the same priority it was added with, which is 1. Without the priority it silently does nothing, and you’re back where you started.

Because this runs inside wp_enqueue_scripts, it happens before wp_footer fires, so the footer enqueue is gone by the time WordPress would have called it.

Is removing global styles safe?

Only if nothing on the page relies on them. Global styles carry the --wp--preset--* custom properties: colour palette, font sizes, spacing scale. They also carry the classes that use them, such as .has-large-font-size. If an editor has picked a preset colour on a paragraph block, that paragraph needs those classes to look right.

That’s why the function above bails out early on any singular view whose content contains blocks. On a Classic Editor site the content is plain HTML, so nothing references the presets. If you have a mix of old block-based posts and newer classic ones, the has_blocks() check keeps the styles exactly where they’re needed.

Two places to double-check before you ship it:

  • Widgets. Block-based widgets can use presets too, and they’re not part of the post content, so has_blocks() won’t see them.
  • Reusable patterns and template parts printed by the theme. If your theme outputs block markup anywhere outside the main content, keep the styles on those views.

The wider lesson

This wasn’t a bug in WordPress. Loading block assets on demand is a sensible change that saves bytes on most classic themes. The problem is that a dequeue only works if it runs after the enqueue it’s undoing, and WordPress moved the enqueue.

Any theme code that removes core output is quietly tied to when core adds it. After a major release, it’s worth spending five minutes on the page source to confirm the things you removed are still gone. A visual check won’t catch this one: the page looks identical, it’s just 9 KB heavier on every request.

If you’d like someone to check what your WordPress theme is loading, that’s part of the speed fixes I do.

Written by

James Knowles

Started in 2021 as an apprentice on a support desk, answering clients directly, then worked through agency teams on WordPress, WooCommerce and Shopify builds. Now leads Ramosa and every project.

More from James

Got a WordPress project?