WordPress / gutenberg

The Block Editor project for WordPress and beyond. Plugin is available from the official repository.
https://wordpress.org/gutenberg/
Other
10.32k stars 4.12k forks source link

Block style variations now use hashed classnames, specificity issues #64856

Open chrisandrew-dev opened 2 weeks ago

chrisandrew-dev commented 2 weeks ago

Description:

Recently, WordPress appears to have changed how block style variations are applied, requiring a hashed classname for styles to be correctly applied. For example, the "outline" button variation now requires a classname in the format is-style-outline--{hash}, whereas previously, is-style-outline was sufficient.

This change has introduced a few issues:

  1. Specificity Conflict: The default button styles now have a higher specificity than the variation styles due to their order of declaration, causing variation styles (without a hash) to be overridden by default styles. For example, :root :where(.wp-element-button, .wp-block-button__link) overrides :root :where(.wp-block-button.is-style-outline>.wp-block-button__link:not(.has-background)).
  2. Difficulty in Applying Consistent Styles: The new hashed classnames make it more challenging to apply consistent styles to static elements that don't appear in the block editor or editable template parts, that previously leveraged semantic classnames defined by Wordpress core or output by theme.json (also heavily encouraged in the developer docs). Additionally, :hover, :focus, etc. styles defined in theme.json (styles.elements.button[":hover"], .etc) aren't applied to the "outline" button variation, for example.

Expected Behavior:

Additional Context: The previous implementation where "is-style-outline" was sufficient allowed for more straightforward styling across various button / link elements, including those outside the block editor.

Suggested Solution: Consider adjusting the order of style declarations or revising the specificity rules to ensure that variation styles override default styles, without relying on individually hashed elements and duplicate rulesets applied in the document head. Additionally, providing a way to opt-out of hashed classnames for consistent styling across different parts of the site would be beneficial (why are they necessary?).

Step-by-step reproduction instructions

  1. Create a button block and apply the "outline" style variation.
  2. Inspect the button and observe that the class is-style-outline--{hash} is used.
  3. Remove the hashed class and fallback to the base "is-style-outline" class. Notice that the default button styles are overriding the variation styles due to higher specificity / order of declaration.

Screenshots, screen recording, code snippet

Edit: Removed because LICEcap only recorded mouse cursor.

Environment info

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

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

talldan commented 2 weeks ago

@chrisandrew-dev Thanks for reaching out.

The hashed classnames are now used to support some of the newer advanced uses of block style variations that have theme.json declarations. They can support inner element and block type styles (e.g. headings/paragraphs/buttons within a group could be styled by a group block style variation). You can also have some pretty complicated nesting situations with those styles where the right style needs to take precedence, for example:

While I didn't work on the feature directly, my understanding is that load order and specificity wouldn't be able to solve this issue alone, so separate classnames are generated. @aaronrobertshaw could add some more detail, though I gathered a lot of this from a similar discussion in slack (https://wordpress.slack.com/archives/C02QB2JS7/p1720158357236439) that Aaron and I were involved in.

The other point you mention about the load order/specificity of the previous block styles, that does seem like an unfortunate consequence of the specificity changes - it was important that those styles don't override the new block style variation selectors, and we also want to keep specificity low, but that means it creates more issues like this. There's a similar discussion in #64225.

There's also an issue now that style variations can be edited in the global styles UI, the older static css implementation can become out of sync with the look of the block style variation, so that needs some thought. Maybe the older block variation styles should only be loaded when there's no theme.json declaration, and then load order adjusted if needed. Or perhaps it's better to generate selectors like is-style-outline, is-style-outline--{hash} { for block style variations (it used to be like this but with completely separate rules, so it generated a lot of extra css in the page).

chrisandrew-dev commented 2 weeks ago

You can also have some pretty complicated nesting situations with those styles where the right style needs to take precedence

Hmm, but at what cost and to whose benefit? Whether or not output by a block instance, applying consistent base styles to common HTML elements (headings, paragraphs, lists buttons, links...) shouldn't be road-blocked by experimental or niche features, in my view.

While I didn't work on the feature directly, my understanding is that load order and specificity wouldn't be able to solve this issue alone, so separate classnames are generated.

The other point you mention about the load order/specificity of the previous block styles, that does seem like an unfortunate consequence of the specificity changes - it was important that those styles don't override the new block style variation selectors, and we also want to keep specificity low, but that means it creates more issues like this. There's a similar discussion in #64225.

Since :where wraps both:root :where(.wp-element-button, .wp-block-button__link) and :root :where(.wp-block-button.is-style-outline>.wp-block-button__link:not(.has-background)), the specificity of all selectors should be 0. So, perhaps specificity isn't the right word. Based on my limited understanding, that only leaves order of appearance, i.e. which rules are loaded last.

Or perhaps it's better to generate selectors like is-style-outline, is-style-outline--{hash} { for block style variations (it used to be like this but with completely separate rules, so it generated a lot of extra css in the page).

"is-style-outline" is still present in the generated markup but has no affect per the above. Duplicate rulesets appear to be injected into the head for every instance of a given style variation on a page, anyway (there are two "outline" button variants on this page):

:root :where(.wp-block-button.is-style-outline--edbaa6e1662d3a7243547640b59b0aca .wp-block-button__link){background: transparent none;background-color: red !important;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--7476662d6524e78ada8b829f8f6fcd91 .wp-block-button__link){background: transparent none;background-color: red !important;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;}
aaronrobertshaw commented 2 weeks ago

Thanks for taking the time to write up the detailed issue @chrisandrew-dev and the ping @talldan 👍

WordPress appears to have changed how block style variations are applied, requiring a hashed classname for styles to be correctly applied

This is correct. As Dan notes this was required to support nested applications of block style variations.

The default button styles now have a higher specificity than the variation styles due to their order of declaration, causing variation styles (without a hash) to be overridden by default styles.

The situation is a little more complex than described here. Which makes this statement perhaps a little incomplete or inaccurate.

For backwards compatibility with classic themes, the default block style variation styles had to be kept in the block library stylesheet for the Button block. This is because classic themes wouldn't get any default variation styles from the core theme.json.

As styles should be loaded in the following order and at the same specificity (0-1-0)

  1. block library styles
  2. root global styles
  3. global element styles
  4. global block type styles
  5. global block style variation styles

it leads to what you are suggesting that default button element and block type styles are overriding the variation's defaults.

You are correct, the theme.json element and block type styles override the block style variation defaults in the block library stylesheet that supports classic themes.

The missing piece though is that those default variation styles were also added to the core theme.json file as a block style variation definition. The result is the defaults added to the core theme.json are loaded last as per the list above. Restoring them such that the variation defaults aren't actually overridden by default block styles.

In short, there are two defaults, one required by classic themes in the block library stylesheet, and a second in the core theme.json for block themes.

I'm not arguing this is an ideal scenario but it was the only functional option for WP 6.6.

Difficulty in Applying Consistent Styles: The new hashed classnames make it more challenging to apply consistent styles to static elements that don't appear in the block editor or editable template parts

I might not be understanding this challenge fully. What is leading to the choice to use block style variations to style non-block elements?

The variation styles should have a higher specificity or be applied in a way that they override the default styles.

This should be the case currently.

There is the unfortunate situation with the default button outline styles also needing to be in the core theme.json file to ensure this. It adds confusion but was unavoidable to maintain backwards compatibility for classic themes.

It should be easier to apply matching styles to static elements without relying on hashed classnames.

I'm not sure what this would look like. Do you have something more concrete in mind that would also satisfy the need for nested block style variations?


my understanding is that load order and specificity wouldn't be able to solve this issue alone, so separate classnames are generated

@talldan's explanation here is spot on.

The other point you mention about the load order/specificity of the previous block styles, that does seem like an unfortunate consequence of the specificity changes

It appears to me there is confusion around which styles are being overridden. The default variation styles in the block library are expected to be overridden, both by theme.json element and block type styles as well as the default block style variation styles in core theme.json.

There's also an issue now that style variations can be edited in the global styles UI, the older static css implementation can become out of sync with the look of the block style variation, so that needs some thought.

Only block style variations that have been registered in the Block Styles registry and have an entry in theme.json are present for further customization via Global Styles. Traditional block styles that enqueue a CSS stylesheet or inline styles aren't editable in Global Styles.

A theme author needs to chose one approach or the other i.e. traditional or global styles.

Maybe the older block variation styles should only be loaded when there's no theme.json declaration, and then load order adjusted if needed.

I might just be having a bad day but I'm not following this suggestion either.


Whether or not output by a block instance, applying consistent base styles to common HTML elements (headings, paragraphs, lists buttons, links...) shouldn't be road-blocked by experimental or niche features, in my view.

Can you expand on how block style variation styles are blocking the application of element styles?

Since :where wraps both:root :where(.wp-element-button, .wp-block-buttonlink) and :root :where(.wp-block-button.is-style-outline>.wp-block-buttonlink:not(.has-background)), the specificity of all selectors should be 0.

The :root selector has 0-1-0 specificity, so together with the 0-0-0 specificity of the :where() wrapped portion the end results is 0-1-0.

"is-style-outline" is still present in the generated markup but has no affect per the above

This is both for backwards compatibility and used by block supports to detect when a variation has been applied to generate the instance specific classname and styles.

Duplicate rulesets appear to be injected into the head for every instance of a given style variation on a page, anyway (there are two "outline" button variants on this page):

It is correct that each instance of a block style variation adds its own style. This is done to solve the nested use case discussed. At this stage, the Button block's variation is treated the same as any other block style variation.

It could be possible to have a list of blocks that do not support nested variations and alter the generation of styles.


Apologies for the wall of text, I hope it helped clarify a few things though! 🤞

FYI, I'll be away for around a week and a half, so unfortunately I won't be able to continue participating in this discussion until early-mid September.

chrisandrew-dev commented 2 weeks ago

@aaronrobertshaw & @ talldan Thanks for the clear, detailed and timely responses - really helpful.

The missing piece though is that those default variation styles were also added to the core theme.json file as a block style variation definition. The result is the defaults added to the core theme.json are loaded last as per the list above. Restoring them such that the variation defaults aren't actually overridden by default block styles.

Aha, now it makes sense why this is happening.

Difficulty in Applying Consistent Styles: The new hashed classnames make it more challenging to apply consistent styles to static elements that don't appear in the block editor or editable template parts

I might not be understanding this challenge fully. What is leading to the choice to use block style variations to style non-block elements?

In the case of buttons, it's to ensure all buttons are styled consistent with our design, and so rulesets applied to one button apply to another (of the same style variation), regardless of whether the button is an instance of a button block or static markup added to a header, footer, form or other PHP template part.

The variation styles should have a higher specificity or be applied in a way that they override the default styles.

This should be the case currently.

There is the unfortunate situation with the default button outline styles also needing to be in the core theme.json file to ensure this. It adds confusion but was unavoidable to maintain backwards compatibility for classic themes.

We've been incrementally adopting new features into our new and existing client sites for some time. This appears to suggest there's no clear migration path from classic themes to block themes..?

It should be easier to apply matching styles to static elements without relying on hashed classnames.

I'm not sure what this would look like. Do you have something more concrete in mind that would also satisfy the need for nested block style variations?

I'm not familiar enough with the issue your team was trying to solve to suggest an alternative. At a fundamental level, and given two identical HTML elements, I'd expect the element with the additional "is-outline-style" class to have the class rulesets applied.

A theme author needs to chose one approach or the other i.e. traditional or global styles.

This is perhaps the most poignant point in the context of developing enhancements to existing projects, or expediting development using our in-house boilerplates.

Whether or not output by a block instance, applying consistent base styles to common HTML elements (headings, paragraphs, lists buttons, links...) shouldn't be road-blocked by experimental or niche features, in my view.

Can you expand on how block style variation styles are blocking the application of element styles?

A PHP project embracing select block theme features can no longer leverage variation classnames to style block and non-block elements (buttons, in this example) consistently, and in one place.

Duplicate rulesets appear to be injected into the head for every instance of a given style variation on a page, anyway (there are two "outline" button variants on this page):

It is correct that each instance of a block style variation adds its own style.

I might have missed something critical but from what I can tell, the duplicate rulesets are identical. There is no unique style property in the examples shown and the entire ruleset is redeclared for each button rather than being scoped to "is-style-outline". I can see this becoming very bloated as I build out the page.

Apologies for the wall of text, I hope it helped clarify a few things though! 🤞

It certainly helped improve my understanding of the changes, thank you. I hope the points raised stimulate further discussion and that hybrid theme developers and agencies like our own have a clear migration path, moving forward.

Enjoy your break!

aaronrobertshaw commented 5 days ago

Thanks for taking the time to provide the continued feedback and clarifications 👍

This appears to suggest there's no clear migration path from classic themes to block themes..?

The issue for classic themes was that they still needed the default styles to belong somewhere when they don't have a theme.json file. I don't think the presence of these styles here prevents any migration of the theme into a block theme.

At a fundamental level, and given two identical HTML elements, I'd expect the element with the additional "is-outline-style" class to have the class rulesets applied.

If you register a block style using the traditional approach of enqueuing a stylesheet, that stylesheet would target the simpler is-style-outline class.

To me, the problem described with applying block styles to static elements is down to trying to repurpose a feature intended to work with blocks only. We might need to see how common a use case this is before the feature could be extended to support it.

A PHP project embracing select block theme features can no longer leverage variation classnames to style block and non-block elements (buttons, in this example) consistently, and in one place.

As eluded to above, if a block style variation is registered along with a stylesheet, the block style variation class will be applied to the block as before. The styles in the block style's stylesheet can target that directly. That class can be applied to static elements if desired and therefore styled as expected.

This issue now looks like it can be isolated somewhat to the button block only as it had a core block style registered and was then also configurable via Global Styles.

One possible workaround, although I'm not sure how palatable it would be, could be to deregister the core Button outline block style, then register a new custom block style using the traditional approach. This wouldn't be configurable via Global Styles and would then use the simple non-unique classname allowing you to use it for your use case.

I might have missed something critical but from what I can tell, the duplicate rulesets are identical. There is no unique style property in the examples shown and the entire ruleset is redeclared for each button rather than being scoped to "is-style-outline".

Maybe not missed exactly but discarded perhaps? The need is to support nested applications of block style variations as outlined in this comment.

Take the following quick example, there's a section (Group) with a variation applied, within that there's a pricing table (Group/Row block) with a different variation applied, and finally in each column there's more content including another group with the section's variation applied.

In this case if the group block style variation applied to the pricing table was defined after the section's and the variations only used the non-unique class is-style-*, the second variation's styles would override the first variation's styles that should apply to the inner Group blocks within the columns of the pricing table.

I can see this becoming very bloated as I build out the page.

This was a known issue but perhaps its severity was underestimated as it was not expected that block style variations were being used as per your use case.

This is why I suggested it could be possible in a future iteration of the feature to have certain block types that wouldn't support nesting variations and therefore wouldn't need the application of variations styles per instance. The button block would be the obvious candidate for this.

Enjoy your break!

🙇 Thanks.