Skip to content

Block styles registered in theme.json generate unnecessary repetitive CSS #81583

Description

@cbirdsong

Description

Selecting block styles adds a class with a suffix like --1, and the exact same CSS is then injected along with it. This is most noticeable with the button outline style.

A single copy of the styles should be output instead of multiple copies.

Step-by-step reproduction instructions

On a block with block styles registered, add multiple copies of the block with that style selected, like so:

<!-- wp:buttons -->
<div class="wp-block-buttons"><!-- wp:button {"className":"is-style-outline"} -->
<div class="wp-block-button is-style-outline"><a class="wp-block-button__link wp-element-button">Button 1</a></div>
<!-- /wp:button -->

<!-- wp:button {"className":"is-style-outline"} -->
<div class="wp-block-button is-style-outline"><a class="wp-block-button__link wp-element-button">Button 2</a></div>
<!-- /wp:button -->

<!-- wp:button {"className":"is-style-outline"} -->
<div class="wp-block-button is-style-outline"><a class="wp-block-button__link wp-element-button">Button 3</a></div>
<!-- /wp:button --></div>
<!-- /wp:buttons -->

On the rendered page you will find the following in a style tag:

:root :where(.wp-block-button.is-style-outline--2 .wp-block-button__link) {
  background: transparent none;
  border-color: currentColor;
  border-width: 2px;
  border-style: solid;
  color: currentColor;
  padding-top: 0.667em;
  padding-right: 1.33em;
  padding-bottom: 0.667em;
  padding-left: 1.33em;
}
:root :where(.wp-block-button.is-style-outline--3 .wp-block-button__link) {
  background: transparent none;
  border-color: currentColor;
  border-width: 2px;
  border-style: solid;
  color: currentColor;
  padding-top: 0.667em;
  padding-right: 1.33em;
  padding-bottom: 0.667em;
  padding-left: 1.33em;
}
:root :where(.wp-block-button.is-style-outline--4 .wp-block-button__link) {
  background: transparent none;
  border-color: currentColor;
  border-width: 2px;
  border-style: solid;
  color: currentColor;
  padding-top: 0.667em;
  padding-right: 1.33em;
  padding-bottom: 0.667em;
  padding-left: 1.33em;
}

Environment info

  • Wordpress 7.0.4, tested with both the Twenty Twenty (hybrid) and Twenty Twenty-Five (block) themes

Please confirm that you have searched existing issues in the repo.

  • Yes

Please confirm that you have tested with all plugins deactivated except Gutenberg.

  • Yes

Please confirm which theme type you used for testing.

  • Block
  • Classic
  • Hybrid (e.g. classic with theme.json)
  • Not sure

Activity

  1. added
    [Type] BugAn existing feature does not function as intended
    on Aug 13, 2026
  2. added theissue type on Aug 13, 2026
  3. added
    Global StylesAnything related to the broader Global Styles efforts, including Styles Engine and theme.json
    CSS StylingRelated to editor and front end styles, CSS-specific issues.
    [Feature] Block Style VariationsIssues or PRs that are related to the style variations for blocks
    on Aug 13, 2026
  4. sanketio commented on Aug 14, 2026

    @sanketio
    Contributor

    I can reproduce this. Some notes on the cause, and a warning about the obvious fix.

    Tested on Gutenberg trunk dbe1c1ccab1 (23.8.0-rc.1), WordPress 7.0.4, Twenty Twenty-Five 1.5, using the exact markup from the report.

    Reproduction

    Outline buttons on page block-style-variation-styles inline CSS Rules emitted Distinct rules
    3 1,663 bytes 6 2
    20 10,782 bytes 40 2

    Growth is linear and unbounded — each instance adds a full copy. Normalising the instance number (--2, --3, --4 → --N) collapses all of it to the same two rules.

    Worth noting there is no rendering difference: computed border colour, width, background and padding are identical no matter how many copies are emitted. This is purely wasted payload.

    It also reproduces with the Gutenberg plugin deactivated on WordPress 7.0.4, so it affects core as well as the plugin.

    Cause

    gutenberg_render_block_style_variation_support_styles() in lib/block-supports/block-style-variations.php (and wp_render_block_style_variation_support_styles() in core) mints a fresh instance for every block on render_block_data:

    $variation_instance = wp_unique_id( $variation . '--' );
    $class_name         = "is-style-$variation_instance";

    The variation data feeding the stylesheet is read from the merged theme.json, and its ref values are resolved against that same tree. Nothing block-specific is involved, so for a given block type and variation the generated CSS is identical every time apart from the scope class.

    Why caching by variation slug is not safe

    This looks like a one-line cache, so it seems worth writing up why it isn't. I tried it, and it silently breaks the cascade.

    Every variation rule is wrapped in :where():

    :root :where(.wp-block-button.is-style-outline--2 .wp-block-button__link) { ... }

    so every variation rule has the same specificity and source order alone decides which one wins. The per-instance class is what places a nested variation's styles after its ancestor's.

    Test case — two group variations that style paragraphs differently, demo-a red and demo-b blue:

    <!-- A standalone -->                   paragraph should be red
    <!-- B, containing A -->                inner paragraph should be red (A is nearer)
    Emission order Inner paragraph
    trunk today demo-a--2 -> demo-b--3 -> demo-a--4 red (correct)
    cache by variation slug demo-a--2 -> demo-b--3 blue (wrong)

    With the slug cache the nested demo-a reuses the class emitted before demo-b, so demo-b wins and the paragraph changes colour.

    One thing that makes this easy to miss: Twenty Twenty-Five's own section styles set descendant colours with currentColor, so nesting section-1 inside section-2 produces byte-identical descendant rules and shows no visible change even when the ordering is broken. Verifying against the bundled section styles alone would suggest the slug cache is fine.

    Suggested approach

    Reuse the previous instance only when the immediately preceding variation instance is the same one. Nothing else was emitted in between, so re-emitting would be a no-op in cascade terms and the class can safely be shared. Anything interleaved forces a fresh instance, so today's ordering is preserved exactly.

    Case Before After
    3 outline buttons 1,663 bytes / 6 rules 593 bytes / 2 rules
    20 outline buttons 10,782 bytes / 40 rules 593 bytes / 2 rules
    Nested section styles 6,130 bytes / 58 rules unchanged, byte for byte
    Nested demo-a / demo-b inner paragraph red still red

    Trade-off worth weighing: this collapses runs of the same variation, which covers the reported case and the common ones (repeated blocks in a Query Loop, sibling buttons), but an alternating A B A B sequence still emits a copy per instance. Fully deduplicating would mean collecting variations during render and emitting them ordered by nesting depth instead of document order — a considerably larger change to how this filter works, and it would need care around blocks rendered outside the main content. Happy to go that route instead if that is preferred.

    Heads-up that #80718 touches the same function, so there may be a small conflict depending on merge order.

    I'll open a PR with this shortly. It includes PHPUnit coverage — one test for the reuse, and one guarding that an interrupting variation still gets a fresh instance; the first fails on trunk without the change.

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

Metadata

Metadata

Assignees

Labels

CSS StylingRelated to editor and front end styles, CSS-specific issues.Global StylesAnything related to the broader Global Styles efforts, including Styles Engine and theme.json[Feature] Block Style VariationsIssues or PRs that are related to the style variations for blocks[Feature] Theme Style VariationsRelated to style variations provided by block themes[Status] In ProgressTracking issues with work in progress[Type] BugAn existing feature does not function as intended

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions