Skip to content

Editor: Generate custom CSS for blocks rendered by their parent block - #14538

Open
ramonjd wants to merge 3 commits into
WordPress:trunkfrom
ramonjd:fix/custom-css-blocks-rendered-by-parent
Open

ramonjd wants to merge 3 commits into
WordPress:trunkfrom
ramonjd:fix/custom-css-blocks-rendered-by-parent

Conversation

@ramonjd

@ramonjd ramonjd commented Oct 20, 2026

Copy link
Copy Markdown
Member

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

Backports WordPress/gutenberg#82590

What

Generates per-block Additional CSS for blocks that a parent block parses and renders itself, such as the links in a navigation menu.

Why

wp_render_custom_css_support_styles() runs on render_block_data. Core applies that filter in render_block() and when WP_Block::render() renders a block's own inner blocks. core/navigation builds a WP_Block_List from a wp_navigation post and calls render() on each block itself, so those blocks never reach the filter. On trunk, Additional CSS on a Navigation Link is dropped on the front end.

What changed

  • Class name generation and stylesheet registration move into a new private helper, wp_generate_custom_css_class_name(). wp_render_custom_css_support_styles() now calls it.
  • wp_render_custom_css_class_name() runs on render_block, which fires for every block however it was rendered. When the block attributes carry no wp-custom-css- class, it now generates one with the helper. The class name is a deterministic hash, so a block that goes through both filters still gets one class and one rule.
  • Three tests ported from the Gutenberg PR. Two fail on trunk. The third guards against a second class being added.

Differences from the Gutenberg PR: the new helper is marked @access private, like the other 7.2.0 helpers in this file, and the test methods drop the render_custom_css_class_name_ prefix because the test class already covers that function.

Manual testing

  1. Using a block theme, edit a template and add a Navigation block backed by a navigation menu, with a Navigation Link inside it.
  2. Select the Navigation block and add margin-top: 0; in Advanced > Additional CSS.
  3. Select the Navigation Link and add padding: 0; in Advanced > Additional CSS.
  4. Save and view the template on the front end. Both :root :where(.wp-custom-css-*) rules should be in the page, and the link's <li> should carry has-custom-css and its wp-custom-css-* class. On trunk, only the Navigation block's rule is output.
  5. Check that Additional CSS on ordinary nested blocks, such as a paragraph inside a group, still works and is not duplicated.

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

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 ramonopoly, mikachan.

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

@ramonjd
ramonjd requested a review from scruffian October 20, 2026 06:42
@ramonjd ramonjd added the bug label Oct 20, 2026
@github-actions

Copy link
Copy Markdown

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

  • 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.


// Direct WP_Block::render() calls skip render_block_data for the block itself.
if ( null === $matched_class_name ) {
$matched_class_name = wp_generate_custom_css_class_name( $block );

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.

One behaviour change worth knowing about.

If a site has switched off block custom CSS by removing this one filter:

remove_filter( 'render_block_data', 'wp_render_custom_css_support_styles' );

Before this PR, that was enough.

After this PR, it isn't, and that site's block custom CSS shows up on the front end again.

That's because the other callback, wp_render_custom_css_class_name on render_block, now calls wp_generate_custom_css_class_name().

That helper outputs the CSS for any block that doesn't have its custom CSS class yet.

To keep custom CSS switched off, a site needs to remove both callbacks:

remove_filter( 'render_block_data', 'wp_render_custom_css_support_styles' );
remove_filter( 'render_block', 'wp_render_custom_css_class_name' );

To switch it off for one block type only, set supports.customCSS to false with the register_block_type_args filter.

The Gutenberg plugin already removes both callbacks, so it isn't affected.

I don't know whether any sites rely on removing just the one filter.

Should this go in the dev note, or is it too small to mention?

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.

Should this go in the dev note, or is it too small to mention?

Yeah, I think we could mention this in the dev note, and I don't think we need to make any code changes. I've had a quick search around on GH, and I can only find one theme (xenolito/vertiGO-web) that removes both callbacks, but it removes the filter to replace it with its own class, not to turn custom CSS off. This should still work with this PR.

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.

Thanks for the confidence check @mikachan 🙇🏻

}

// Direct WP_Block::render() calls skip render_block_data for the block itself.
if ( null === $matched_class_name ) {

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.

Alternative might be

if (
	null === $matched_class_name &&
	false !== has_filter(
		'render_block_data',
		'wp_render_custom_css_support_styles'
	)
) {
	$matched_class_name = wp_generate_custom_css_class_name( $block );
}

That does couple the fallback to the registration state of a private callback. I think it's okay.

A cleaner long term design would be an explicit opt-out filter checked by wp_generate_custom_css_class_name(). Sites that currently use remove_filter() would need to migrate to it.

// after the block supports check
if ( ! apply_filters( 'wp_should_render_block_custom_css', true, $parsed_block ) ) {
	return null;
}

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.

I'm not sure we need to make either of these changes now. The has_filter() guard would stop the navigation fix for sites that replace the filter rather than remove it, like the theme I mentioned above. We're not sure if a new wp_should_render_block_custom_css filter is needed yet, and supports.customCSS already covers turning it off per block type. Could we handle these in follow-ups if they're required?

@ramonjd
ramonjd force-pushed the fix/custom-css-blocks-rendered-by-parent branch from 588e2ba to d3404d8 Compare October 21, 2026 02:44
`wp_render_custom_css_support_styles()` runs on `render_block_data`, which is applied in `render_block()` and when `WP_Block::render()` renders a block's own inner blocks. Blocks that a parent parses and renders itself never reach it. For example, `core/navigation` builds a `WP_Block_List` from a `wp_navigation` post and calls `render()` on each block, so the Additional CSS of a Navigation Link was dropped on the front end.

Class name generation and stylesheet registration move into a new private helper, `wp_generate_custom_css_class_name()`. `wp_render_custom_css_class_name()` runs on `render_block` for every block, and now calls the helper when the block attributes carry no `wp-custom-css-` class. The class name is a deterministic hash, so a block that passes through both filters still gets one class and one rule.

Backports WordPress/gutenberg#82590.

See #66345.
@ramonjd
ramonjd force-pushed the fix/custom-css-blocks-rendered-by-parent branch from d3404d8 to 12a1a09 Compare October 21, 2026 08:03
…sts.

The Navigation menu test renders `core/navigation`, which enqueues the Navigation view script module. The queue was not reset between tests, so the module leaked into later test classes and broke `Tests_Blocks_Register::test_register_block_script_module_id_with_interactivity_true` when the full suite ran.

See #66345.

@mikachan mikachan left a comment

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.

Thanks @ramonjd! This backports WordPress/gutenberg#82590 successfully, and I can see the tests are happy now. I think this is ready to bring in 🚢

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants