Opened 5 weeks ago
Closed 4 weeks ago
#65435 closed defect (bug) (fixed)
wp_get_elements_class_name() generates duplicate class names for identical blocks, causing link color CSS cascade failure
| Reported by: | tusharbharti | Owned by: | westonruter |
|---|---|---|---|
| Priority: | normal | Milestone: | 7.1 |
| Component: | Editor | Version: | |
| Severity: | normal | Keywords: | has-patch has-unit-tests |
| Cc: | Focuses: |
Description
When a block (e.g. Paragraph) has the same content and styling as another block elsewhere on the page, wp_get_elements_class_name() in wp-includes/block-supports/elements.php generates the same wp-elements-* CSS class for both, because it relies solely on md5( serialize( $block ) ).
The Style Engine deduplicates the resulting CSS rule.
If a parent block (e.g. Group) also has a conflicting element style (like a link color), the parent's rule is output after the child's in the stylesheet due to DOM traversal order, causing the parent to incorrectly override the child's style via standard CSS source-order precedence.
Example scenario:
- Standalone Paragraph block with a blue link color
- Group block with a red link color containing a Paragraph block with a blue link color
Both Paragraph blocks have identical content, so they produce the same wp-elements-* class. The Group's red link rule appears later in the CSS, overriding the inner Paragraph's blue link — even though the inner Paragraph should take priority.
The fix for this require changes to wp_get_elements_class_name() hence a core trac ticket has been created.
More details on https://github.com/WordPress/gutenberg/issues/53803
Change History (16)
This ticket was mentioned in PR #12126 on WordPress/wordpress-develop by @tusharbharti.
5 weeks ago
#1
- Keywords has-patch has-unit-tests added
@westonruter commented on PR #12126:
5 weeks ago
#2
I took this as an opportunity to fix up some typing issues on code that touches wp_get_elements_class_name().
@westonruter commented on PR #12126:
5 weeks ago
#4
@t-hamano If this looks good to you, I can commit.
@westonruter commented on PR #12126:
4 weeks ago
#5
@USERSATOSHI Why did you revert the hash removal?
@tusharbharti commented on PR #12126:
4 weeks ago
#6
After adding it, All the test started to fail and reverting that hash was the only way to make the tests pass.
@tusharbharti commented on PR #12126:
4 weeks ago
#7
https://github.com/WordPress/wordpress-develop/actions/runs/27536856548/job/81388480559?pr=12126
action link after that commit
@wildworks commented on PR #12126:
4 weeks ago
#8
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?
@tusharbharti commented on PR #12126:
4 weeks ago
#9
sure, let me check this.
@tusharbharti commented on PR #12126:
4 weeks ago
#10
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?
@wildworks commented on PR #12126:
4 weeks ago
#11
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?
---
- 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 ); }
---
- 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-' ); }
---
- Remove the argument. No deprecation warnings.
function wp_get_elements_class_name(): string { return wp_unique_prefixed_id( 'wp-elements-' ); }
---
- 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 commented on PR #12126:
4 weeks ago
#12
@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-' ); }
@tusharbharti commented on PR #12126:
4 weeks ago
#13
got it, I will start with this then!
@westonruter commented on PR #12126:
4 weeks ago
#14
Given a post with the following post_content:
<p class="has-link-color"><a href="#">This paragraph block text is linked.</a></p> <div class="wp-block-group alignwide has-link-color"> <p class="has-link-color"><a href="#">This paragraph block text is linked.</a></p> </div>
Before | After
The diff before/after on the rendered HTML:
-
.html
old new 2371 2371 gap: 0.2em; 2372 2372 justify-content: space-between; 2373 2373 } 2374 .wp-elements-ce1bfdc6f939ea169c1a504d63b15129 2375 a:where(:not(.wp-element-button)) { 2374 .wp-elements-1 a:where(:not(.wp-element-button)) { 2376 2375 color: var(--wp--preset--color--vivid-cyan-blue); 2377 2376 } 2378 .wp-elements-61302282145b76b02958cdbaffbdf258 2379 a:where(:not(.wp-element-button)) { 2377 .wp-elements-2 a:where(:not(.wp-element-button)) { 2380 2378 color: var(--wp--preset--color--vivid-red); 2379 } 2380 .wp-elements-3 a:where(:not(.wp-element-button)) { 2381 color: var(--wp--preset--color--vivid-cyan-blue); 2381 2382 } 2382 2383 .wp-container-core-group-is-layout-878fe601 { 2383 2384 flex-wrap: nowrap; … … 2895 2896 <div 2896 2897 class="entry-content alignfull wp-block-post-content has-global-padding is-layout-constrained wp-block-post-content-is-layout-constrained" 2897 2898 > 2898 <p 2899 class="has-link-color wp-elements-ce1bfdc6f939ea169c1a504d63b15129 wp-block-paragraph" 2900 > 2899 <p class="has-link-color wp-elements-1 wp-block-paragraph"> 2901 2900 <a href="#">This paragraph block text is linked.</a> 2902 2901 </p> 2903 2902 2904 2903 <div 2905 class="wp-block-group alignwide has-link-color wp-elements- 61302282145b76b02958cdbaffbdf258has-global-padding is-layout-constrained wp-block-group-is-layout-constrained"2904 class="wp-block-group alignwide has-link-color wp-elements-2 has-global-padding is-layout-constrained wp-block-group-is-layout-constrained" 2906 2905 > 2907 <p 2908 class="has-link-color wp-elements-ce1bfdc6f939ea169c1a504d63b15129 wp-block-paragraph" 2909 > 2906 <p class="has-link-color wp-elements-3 wp-block-paragraph"> 2910 2907 <a href="#">This paragraph block text is linked.</a> 2911 2908 </p> 2912 2909 </div>
@westonruter commented on PR #12126:
4 weeks ago
#15
@dpmehta If you can share your WordPress.org username, I can add you to the props list for the commit _post hoc_.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
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 viamd5( 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.phpis required beforelayout.phpinwp-settings.php, sowp_render_elements_support_styles()runs beforewp_add_parent_layout_to_parsed_block().If layout ran first, nested blocks would have a
parentLayoutkey, producing different hashes.### Fix
Introduces a collision tracker via
static $seen_hashesinwp_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_blockstests/phpunit/tests/block-supports/wpRenderElementsSupport.php::test_elements_block_support_class_with_duplicate_blocks#### Screenshots
### Credits
### 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.