Skip to content

Editor: Fix link color CSS cascade conflict for duplicate blocks with identical markup#12126

Open
USERSATOSHI wants to merge 16 commits into
WordPress:trunkfrom
USERSATOSHI:fix/elements-class-name-collision
Open

Editor: Fix link color CSS cascade conflict for duplicate blocks with identical markup#12126
USERSATOSHI wants to merge 16 commits into
WordPress:trunkfrom
USERSATOSHI:fix/elements-class-name-collision

Conversation

@USERSATOSHI

Copy link
Copy Markdown

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

Description

This fixes a CSS cascade bug where identical block content produces duplicate wp-elements-* class names, causing parent block element styles (e.g. link colors) to incorrectly override child block styles.

Root cause

wp_get_elements_class_name() generates CSS class names via md5( serialize( $block ) ).
When two blocks have identical parsed data (e.g. two Paragraph blocks with the same text, one standalone and one inside a Group), they produce the same class.
The Style Engine deduplicates the rule, and since the parent Group's rule renders later in DOM order, it overrides the child Paragraph's style.

A contributing factor: elements.php is required before layout.php in wp-settings.php, so wp_render_elements_support_styles() runs before wp_add_parent_layout_to_parsed_block().
If layout ran first, nested blocks would have a parentLayout key, producing different hashes.

Fix

Introduces a collision tracker via static $seen_hashes in wp_get_elements_class_name(). When a hash collision is detected, an incrementing counter is appended before re-hashing, producing a unique class name per duplicate instance. This ensures the Style Engine outputs distinct CSS rules for each block instance, preserving correct cascade order.

Testing

Added Test cases for these cases.

  • tests/phpunit/tests/block-supports/wpRenderElementsSupportStyles.php::test_elements_block_support_styles_with_duplicate_blocks
  • tests/phpunit/tests/block-supports/wpRenderElementsSupport.php::test_elements_block_support_class_with_duplicate_blocks

Screenshots

Before After
image image

Credits

  • @dpmehta: Initial investigation, root cause analysis, and Gutenberg fix prototype.

References

Use of AI Tools

AI assistance: Yes
Tool(s): OpenCode
Model(s): deepseek-v4-flash-free
Used for: I used AI only for creating the test scaffolding. Code is reviewed and written by me.


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.

@github-actions

github-actions Bot commented Jun 8, 2026

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.

@USERSATOSHI USERSATOSHI marked this pull request as ready for review June 8, 2026 14:30
@github-actions

github-actions Bot commented Jun 8, 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 tusharbharti, westonruter, wildworks.

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

Comment thread src/wp-includes/block-supports/elements.php Outdated
*
* @covers ::wp_get_elements_class_name
*/
public function test_elements_block_support_styles_with_duplicate_blocks(): void {

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.

Do we need this test? Is it not the same as in Tests_Block_Supports_WpRenderElementsSupport?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It is almost the same, but they test different outputs.

the Tests_Block_Supports_WpRenderElementsSupport one checks for the html output
while this one checks for the styles part.

so something like <p class="wp-elements-abc123"> vs .wp-elements-abc123 a { color: blue; }

@westonruter

Copy link
Copy Markdown
Member

I took this as an opportunity to fix up some typing issues on code that touches wp_get_elements_class_name().

Comment on lines +165 to +167
if ( ! $block_type ) {
return $parsed_block;
}

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.

This condition handles a possible case where the parsed block doesn't exist in the registry.

@westonruter westonruter requested a review from t-hamano June 10, 2026 19:17
Comment on lines +117 to +129
$actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) );

// Both rules should be present with distinct class names.
$this->assertMatchesRegularExpression(
'/\.wp-elements-[a-f0-9]{32}[0-9]+ a:where\(:not\(\.wp-element-button\)\)\{color:blue;\}/',
$actual_stylesheet,
'First block element style should be present'
);
$this->assertMatchesRegularExpression(
'/\.wp-elements-[a-f0-9]{32}[0-9]+ a:where\(:not\(\.wp-element-button\)\)\{color:blue;\}/',
$actual_stylesheet,
'Second block element style should also be present'
);

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.

This doesn't make sense to me. It's asserting the same thing twice. In any case, doing a pattern match on the entire CSS rule seems maybe excessive and is liable to break when the selector changes. I think this can be simplified. See f944d13.

@westonruter

Copy link
Copy Markdown
Member

@t-hamano If this looks good to you, I can commit.

@t-hamano t-hamano left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the PR! This PR works well in my tests.

I am wondering if a hash value is necessary at all if we are using wp_unique_prefixed_id(). I am thinking if we can simply do the following:

function wp_get_elements_class_name( $parsed_block ): string {
	return wp_unique_prefixed_id( 'wp-elements-' );
}

Since the CSS class name is guaranteed to be unique by wp_unique_prefixed_id(), there should be no need to generate a hash value. This simplifies CSS class names to be like wp-elements-1, wp-elements-2, etc.

This should match the implementation in the editor.

https://github.com/WordPress/gutenberg/blob/ffd8f0064cf049c5406b1f9e0d39eea52d82002d/packages/block-editor/src/hooks/style.js#L921-L924

What do you think?

@westonruter

Copy link
Copy Markdown
Member

@USERSATOSHI Why did you revert the hash removal?

@USERSATOSHI

Copy link
Copy Markdown
Author

After adding it, All the test started to fail and reverting that hash was the only way to make the tests pass.

@USERSATOSHI

Copy link
Copy Markdown
Author

@t-hamano

Copy link
Copy Markdown
Contributor

Failed asserting that '<p class="wp-elements-2">Hello <a href="http://www.wordpress.org/">WordPress</a>!</p>' matches PCRE pattern "/^<p class="wp-elements-[a-f0-9]{32}[0-9]+">Hello <a href="http:\/\/www.wordpress.org\/">WordPress<\/a>!<\/p>$/".

Since we changed the logic for the CSS output, it's natural that the unit tests are failing. Can we fix the tests?

@USERSATOSHI

Copy link
Copy Markdown
Author

sure, let me check this.

@USERSATOSHI

Copy link
Copy Markdown
Author

Hi @t-hamano @westonruter , I just realized while updating the tests that we also dont need parsed_block parameter for the function now since it doesn't really on it.

so do I also update this for all the places where this function is used?

@t-hamano

Copy link
Copy Markdown
Contributor

I'm starting to wonder if removing the arguments is truly the best approach, as I have concerns about backward compatibility. Although this function is marked as @access private, it is effectively a public function that anyone can use.

As far as I can think of, there are four approaches. @westonruter What do you think?


  1. Maintain the argument $parsed_block, hash it, and use it as part of the class name.
function wp_get_elements_class_name( $parsed_block ): string {
	$hash = md5( serialize( $parsed_block ) );
	return wp_unique_prefixed_id( 'wp-elements-' . $hash );
}

  1. Maintain the argument $parsed_block, but don't use it as part of the class name.
function wp_get_elements_class_name( $parsed_block ): string {
	return wp_unique_prefixed_id( 'wp-elements-' );
}

  1. Remove the argument. No deprecation warnings.
function wp_get_elements_class_name(): string {
	return wp_unique_prefixed_id( 'wp-elements-' );
}

  1. Remove the argument. Deprecation warning included.
function wp_get_elements_class_name(): string {
	if ( func_num_args() > 0 ) {
		_deprecated_argument(
			__FUNCTION__,
			'7.1.0',
			__( 'The $parsed_block parameter is no longer used.' )
		);
	}
	return wp_unique_prefixed_id( 'wp-elements-' );
}

@westonruter

Copy link
Copy Markdown
Member

@t-hamano Either 3 or 4 looks good to me. It doesn't hurt to pass in an argument that isn't expected. Static analysis in an editor will flag it, but it won't be any kind of runtime error. So calling _deprecated_argument() would be somewhat excessive. This is even moreso because it is only ever used in one single place: wp_render_elements_support_styles(). That said, there are 2 plugins (besides Gutenberg) which are passing in $parsed_block: https://veloria.dev/search/809a255b-509a-41a7-bfb7-1e36e89e6a68

Is it worth it to warn them that they're passing in an unused argument at runtime? Probably not. It would also be annoying if they have to support multiple versions of WordPress, since then they'd have to do a version check for whether to pass in the $parsed_block or not.

So I'd say go with:

3. Remove the argument. No deprecation warnings.

function wp_get_elements_class_name(): string {
	return wp_unique_prefixed_id( 'wp-elements-' );
}

@USERSATOSHI

Copy link
Copy Markdown
Author

got it, I will start with this then!

@t-hamano t-hamano left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good to me from my end 👍

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.

3 participants