Block styles that show up fine in the editor and then disappear on the live site are usually a hook-order problem. The main entity here is enqueue_block_assets, a hook that fires in both the editor and the frontend, but with different timing and context than enqueue_block_editor_assets. Adjacent concepts include wp_enqueue_scripts, admin_enqueue_scripts, should_load_separate_core_block_assets, and the block_assets registration path in WP_Block_Type_Registry. For small-to-mid publishing teams that maintain their own production installs, this failure mode usually means a stylesheet is registered in the editor context but never enqueued for site visitors, or it is enqueued too early and then dequeued by a later dependency check. The result is a visual mismatch between what editors approve and what readers see.
This article walks through the exact hook order, reproduces the failure with a minimal plugin, and shows how to inspect the enqueue chain with WP-CLI and SQL. No theory-only advice. Every claim is tied to a snippet you can run on a staging install.
What enqueue_block_assets Actually Does
enqueue_block_assets is documented as firing when block assets are enqueued for both the editor and the frontend. In core, the hook is triggered inside wp_common_block_scripts_and_styles(), which runs on the wp_enqueue_scripts action for the frontend and on enqueue_block_editor_assets for the editor. That dual context is the source of most confusion.
If you register a style with wp_enqueue_style() directly on enqueue_block_assets, it will load in both contexts. But if you wrap the call in an is_admin() check, or if you use wp_register_style() on enqueue_block_assets and then enqueue it only inside an editor-specific callback, the frontend never sees it. The opposite failure also happens: a style is enqueued on enqueue_block_assets but a later wp_dequeue_style() call on wp_enqueue_scripts removes it before the page renders.
The Hook Order in a Default Theme
On a standard frontend request with a block theme, the relevant order is:
wp_enqueue_scriptsfires.- Core calls
wp_common_block_scripts_and_styles()on that action. - Inside that function,
enqueue_block_assetsfires. - Registered block styles from
WP_Block_Type_Registryare enqueued. - Theme and plugin styles enqueued on
wp_enqueue_scriptswith a later priority are added. - WordPress prints the styles in the
wp_headoutput.
In the editor, the order is different. The enqueue_block_editor_assets action fires after the editor script is loaded, and wp_common_block_scripts_and_styles() is called again, which triggers enqueue_block_assets a second time. That means a callback on enqueue_block_assets can run twice on an editor screen: once for the editor context and once for the frontend context if the editor page also loads frontend assets for previews.
Reproducing the Failure with a Minimal Plugin
Create a plugin with this code:
add_action( 'enqueue_block_assets', function () {
wp_register_style(
'jvs-editor-only-style',
plugin_dir_url( __FILE__ ) . 'editor-only.css',
[],
'1.0.0'
);
} );
add_action( 'enqueue_block_editor_assets', function () {
wp_enqueue_style( 'jvs-editor-only-style' );
} );
In the block editor, the style loads because enqueue_block_editor_assets runs after enqueue_block_assets has registered the handle. On the frontend, the style never loads because nothing enqueues the handle after registration. The editor shows the styled block; the published page does not.
Now reverse the pattern:
add_action( 'enqueue_block_assets', function () {
wp_enqueue_style(
'jvs-frontend-style',
plugin_dir_url( __FILE__ ) . 'frontend.css',
[],
'1.0.0'
);
} );
add_action( 'wp_enqueue_scripts', function () {
wp_dequeue_style( 'jvs-frontend-style' );
}, 20 );
Here the style is enqueued on enqueue_block_assets, but the later wp_enqueue_scripts callback with priority 20 dequeues it. The editor still shows the style because the dequeue callback does not run in the editor context. This is a common pattern when a developer tries to conditionally remove a style for certain templates but accidentally removes it everywhere on the frontend.
Inspecting the Enqueue Chain with WP-CLI
To see which styles are registered and enqueued on a given page, use WP-CLI with a small must-use plugin that dumps the global wp_styles object at the wp_footer action:
add_action( 'wp_footer', function () {
global $wp_styles;
if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::log( 'Registered styles:' );
foreach ( $wp_styles->registered as $handle => $style ) {
WP_CLI::log( $handle . ' => ' . $style->src );
}
WP_CLI::log( 'Enqueued styles:' );
foreach ( $wp_styles->queue as $handle ) {
WP_CLI::log( $handle );
}
}
}, 99 );
Run wp eval-file dump-styles.php on a frontend URL and compare the output with the editor screen. The difference between the two lists is your missing stylesheet.
For a database-level check, query the postmeta table for block style metadata that might be stored per post:
SELECT post_id, meta_key, meta_value
FROM wp_postmeta
WHERE meta_key LIKE '%_wp_block_styles%'
ORDER BY post_id DESC
LIMIT 20;
This is useful when a block style is applied only to a specific post in the editor but the frontend render does not include the style because the block type is not registered on the frontend.
Why the Frontend Render Path Is Different
The block editor uses the WP_Block_Type object to render previews, and that object includes the style and editor_style properties. On the frontend, the render path goes through render_block(), which does not automatically enqueue block styles. Core only enqueues block styles on the frontend if the block is present in the post content and the should_load_separate_core_block_assets filter returns true. For custom blocks, the developer must enqueue the style manually on enqueue_block_assets or wp_enqueue_scripts.
This is the second most common cause: a custom block registers its style with editor_style in block.json, which loads only in the editor. The style property is supposed to load on both, but if the block is registered with register_block_type() and the style handle is not enqueued on the frontend, the style never appears. The fix is to add a separate wp_enqueue_style() call on enqueue_block_assets for the frontend handle.
Checking block.json Registration
Run this WP-CLI command to see how a block type is registered:
wp eval 'print_r( WP_Block_Type_Registry::get_instance()->get_registered( "namespace/block-name" ) );'
Look for the style and editor_style properties. If style is missing or points to a handle that is never enqueued, that is your frontend gap.
Fixing the Hook Order Without Breaking the Editor
The reliable pattern is to enqueue frontend styles on enqueue_block_assets and editor-only styles on enqueue_block_editor_assets. Do not use is_admin() inside enqueue_block_assets to decide whether to enqueue a style, because the hook fires in both contexts and the check will be true in the editor and false on the frontend, which is exactly the bug you are trying to avoid.
If you need a style on both, use a single callback on enqueue_block_assets with no context check:
add_action( 'enqueue_block_assets', function () {
wp_enqueue_style(
'jvs-both-contexts',
plugin_dir_url( __FILE__ ) . 'both.css',
[],
'1.0.0'
);
} );
If you need a style only in the editor, use enqueue_block_editor_assets. If you need a style only on the frontend, use wp_enqueue_scripts with a priority after enqueue_block_assets has fired, or use enqueue_block_assets and then conditionally dequeue on wp_enqueue_scripts only for the specific templates where you do not want it.
When the Theme Is the Culprit
Block themes sometimes enqueue styles on after_setup_theme or wp_enqueue_scripts with a priority that runs before enqueue_block_assets. If the theme registers a style handle that a block also uses, the block’s enqueue call may be ignored because the handle is already registered with a different source. Check the theme’s functions.php for wp_register_style() calls that use the same handle as your block style.
Use this WP-CLI command to list all registered style handles and their sources on a frontend page:
wp eval 'global $wp_styles; foreach ( $wp_styles->registered as $handle => $style ) { echo $handle . " => " . $style->src . "\n"; }'
If your block style handle appears with a theme URL instead of your plugin URL, the theme is overriding it. Rename your handle or enqueue with a higher priority.
FAQ
Why does my block style load in the editor but not on the frontend?
Most likely the style is registered on enqueue_block_assets but only enqueued on enqueue_block_editor_assets, or the block’s block.json uses editor_style instead of style. Check the enqueue chain with WP-CLI and compare the editor and frontend style queues.
Can I use is_admin() inside enqueue_block_assets to conditionally load styles?
No. enqueue_block_assets fires in both the editor and the frontend. Using is_admin() inside that hook will return true in the editor and false on the frontend, which recreates the exact bug. Use separate hooks for editor-only and frontend-only styles.
How do I check if a block style is registered but not enqueued?
Dump the global wp_styles object at wp_footer with a must-use plugin and WP-CLI. Compare the registered array with the queue array. If your handle is in registered but not in queue, it is registered but never enqueued on that page.
What is the correct hook for a style that should load on both the editor and the frontend?
Use enqueue_block_assets with a single wp_enqueue_style() call and no context check. That hook fires in both contexts, so the style will be enqueued for both.
For a related failure mode where a new WordPress site returns nothing on archive pages, see What to Fix First When a New WordPress Site Says Nothing Found.


