WordPress / gutenberg

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

Adopt a classes-first approach to CSS #54033

Open cbirdsong opened 1 year ago

cbirdsong commented 1 year ago

Instead of using a discrete stylesheet, many editor features use dynamically generated CSS injected onto the page, either via as a style attribute on an element or in a dynamically generated <style> tag elsewhere on the page.[^terminology]

Injected CSS causes a number of problems for developers working on themes, plugins and even the editor itself. I'm not the first one[^credits] to complain about it, though I hope this issue can serve as a summary. Here are some issues others have raised in the past:

(Also, I'm sure I haven't been comprehensive here - please chime in with any additional related issues or examples of injected CSS in use that I haven't accounted for.)

What problem does this address?

Here's my attempt to break all those issues down into discrete problems:

1. It's difficult to work alongside while respecting the user's choices

When customizing a block's styles[^theme-json-styles] it can be useful to scope those customizations based on the block's current classes. For instance, if I was trying to make a style that looks like a sticky note I can set a default yellow background color because has-background is added when the user customizes the block:

.wp-block-group.is-style-sticky-note:not(.has-background) {
  background-color: palegoldenrod;
}

This way, if the user chooses a different color it will still be honored.

However, I can't do the same for block spacing because that information is purely located within the injected <style> tag. This currently can't work:

.wp-block-group.is-style-sticky-note:not([class*="has-spacing-"]) {
  gap: 0.75em;
}

Full example: https://codepen.io/cbirdsong/pen/VwqLvEr?editors=1100

2. It's difficult to override when necessary

Sometimes the styles the editor is using aren't appropriate for the site due to design constraints or browser support requirements, and in this case injected CSS is harder to override.

For example, I wanted the "full height" setting on the cover block to be slightly less than 100vh to account for a site's sticky nav bar. This was only possible using a hacky selector (.wp-block-cover[style*="100vh"]) and !important.[^cover-height]

On another project, I was creating a masonry style for the gallery block, which required using CSS grid instead of flexbox. The editor still generated the following CSS, even when gap was not customized for that particular block[^gallery-layout]:

.wp-block-gallery.wp-block-gallery-8 {
    --wp--style--unstable-gallery-gap: var( --wp--style--gallery-gap-default, var( --gallery-block--gutter-size, var( --wp--style--block-gap, 0.5em ) ) );
    gap: var( --wp--style--gallery-gap-default, var( --gallery-block--gutter-size, var( --wp--style--block-gap, 0.5em ) ) );
}

I again had to use !important to set the gap amount, which meant I couldn't respect a custom spacing preset chosen by the user.

If these instead used semantic classes it would be easier to override them in a controlled way when necessary.

3. It's impossible to add fallbacks for progressive enhancement

When writing bleeding-edge CSS it's best is to include fallback values for older browsers, but the editor doesn't really support this pattern[^progressive-enhancement]. It has also been delivering relatively bleeding-edge CSS:

If classes were used instead of injected styles then theme developers that needs broader browser support could easily generate their own classes with fallbacks, and developers working on the editor could also write more defensive styles for newer CSS features like dynamic viewport units and container query units.

4. It's impossible to use a strict content security policy

As noted by @huubl in #50417, injected CSS makes the editor incompatible with a strict content security policy. Right now lots of layout stuff on a hybrid theme would break, and a site using FSE would become basically unusable. It should be possible to use WordPress with a strict CSP, and right now it isn't.

5. It generates redundant styles.

The amount of injected CSS the editor generates for layout scales with number of blocks[^redundant-layout-styles]. This is bad for page speed since more HTML is delivered and more CSS needs to be parsed. This effect is worse the larger the page gets. (Detailed example in solution section below)

6. It encourages sloppy CSS.

The CSS that ships in WordPress core should be an example to follow, and it very often just isn't. Some selectors the editor generates:

The fact that all these rules are dynamically generated and can't just be viewed in a static stylesheet makes it harder for other authors to understand on what kind of styles might be applied to a block, which contributes to the use of these kind of ridiculous specificity hacks. With proper semantic class names it's much easier to write styles to accomplish the same goal without hacks.

7. It is often incompatible with cascade layers. (added April 2024)

You could generate <style> tags with @layer in them, but inline styles always beat cascade layers: https://codepen.io/cbirdsong/pen/gOyXzYW


What is your proposed solution?

Use standardized semantic classes to reduce the amount of injected CSS as much as possible. Injected CSS should only be necessary when the user has entered a custom value.

1. Create traditional stylesheets for everything the style engine outputs

The editor should ship basic styles for layout in a traditional stylesheet, which would mean:

I am aware that part of the idea behind the style engine was to only generate the styles necessary for the current page, which is definitely beneficial for page performance. To avoid a regression, the style engine could instead generate only the necessary semantic classes, which would likely be even more efficient than wp-container- classes:

Example CSS from a test page, and what it could be replaced with What the editor ships: ```css .wp-container-9.wp-container-9 { justify-content: center; } .wp-container-19.wp-container-19 { flex-wrap: nowrap; flex-direction: column; align-items: flex-start; } .wp-container-20.wp-container-20 { flex-wrap: nowrap; flex-direction: column; align-items: center; } .wp-container-21.wp-container-21 { flex-wrap: nowrap; flex-direction: column; align-items: flex-end; } .wp-container-2.wp-container-2, .wp-container-10.wp-container-10 { justify-content: flex-end; } .wp-container-3.wp-container-3, .wp-container-11.wp-container-11, .wp-container-50.wp-container-50 { justify-content: space-between; } .wp-container-6.wp-container-6, .wp-container-7.wp-container-7, .wp-container-16.wp-container-16, .wp-container-31.wp-container-31, .wp-container-32.wp-container-32, .wp-container-42.wp-container-42, .wp-container-43.wp-container-43 { flex-direction: column; align-items: flex-start; } .wp-container-12.wp-container-12, .wp-container-25.wp-container-25, .wp-container-26.wp-container-26, .wp-container-36.wp-container-36, .wp-container-37.wp-container-37 { flex-wrap: nowrap; } .wp-container-13.wp-container-13, .wp-container-27.wp-container-27, .wp-container-38.wp-container-38 { flex-wrap: nowrap; justify-content: center; } .wp-container-14.wp-container-14, .wp-container-28.wp-container-28, .wp-container-39.wp-container-39 { flex-wrap: nowrap; justify-content: flex-end; } .wp-container-15.wp-container-15, .wp-container-29.wp-container-29, .wp-container-40.wp-container-40 { flex-wrap: nowrap; justify-content: space-between; } .wp-container-17.wp-container-17, .wp-container-33.wp-container-33, .wp-container-44.wp-container-44 { flex-direction: column; align-items: center; } .wp-container-18.wp-container-18, .wp-container-34.wp-container-34, .wp-container-45.wp-container-45 { flex-direction: column; align-items: flex-end; } ``` This could all be replaced with: ```css .is-nowrap { flex-wrap: nowrap; } .is-layout-flex.is-vertical { flex-direction: column; } .is-layout-flex.is-content-justification-left { justify-content: flex-start; } .is-layout-flex.is-content-justification-center { justify-content: center; } .is-layout-flex.is-content-justification-right { justify-content: flex-end; } .is-layout-flex.is-content-justification-space-between { justify-content: space-between; } .is-layout-flex.is-content-justification-stretch { justify-content: stretch; } .is-layout-flex.is-vertical.is-content-justification-left { align-items: flex-start; } .is-layout-flex.is-vertical.is-content-justification-center { align-items: center; } .is-layout-flex.is-vertical.is-content-justification-right { align-items: flex-end; } .is-layout-flex.is-vertical.is-content-justification-stretch { align-items: stretch; } ```

The CSS shipped by the editor:

The replacement CSS using semantic class names:

2. Minimize the use of <style> blocks/.wp-container- classes

As noted above, it should be possible to reuse simple semantic classes in the place of .wp-container-XX classes for all basic layout settings, as they should only be necessary when a custom spacing value is used on a flow layout.

3. Replace inline CSS with classes wherever possible

Examples include:

These should all be replaced with equivalent classes like is-full-height or has-text-transform-uppercase.

4. Accept presets in every field in the editor where you can enter custom values

The editor still contains features that are only usable with custom values, which require injected CSS. A likely-incomplete list:

These should use presets that generate classes by default and only use injected CSS for custom values entered by the user.

5. Support for disabling entry of custom values

Theme authors should have ability to universally disable every place that users can enter custom values, similar to add_theme_support( 'disable-custom-font-sizes' ). The editor's developers should take that into account whenever new features are added and make sure that injected CSS is only required for custom values, which will mean that the site will generally remain functional under a strict CSP.

6. Add classes to signal usage of custom values

Using injected CSS may be unavoidable with custom values, but more classes likehas-background and has-text-color could be added to make them easier to work with.

Possible examples:


Making these changes would make the CSS the editor generates more comprehensible, flexible and performant, and it would allow theme developers/site owners the ability lock in their designs and secure their sites without unnecessarily compromising on features.

[^terminology]: I'm going to refer to both of these as "injected CSS" since "inline CSS" specifically refers to the style attribute.

[^credits]: Notably: @afercia, @huubl, @markhowellsmead, @mrwweb, @langhenet, @daviedR, @dbushell

[^theme-json-styles]: I know that the recommended way to handle styling global defaults is via theme.json, but sometimes you can't get the job done without actually writing CSS.

[^gallery-layout]: This is also output when layout styles are disabled, as noted in #47948

[^redundant-layout-styles]: Absurdly, many blocks that support layout have classes like is-nowrap and is-content-justification-center, but those classes are unused and injected CSS is still generated to add the required flex-wrap: nowrap; and justify-content: center;.

[^progressive-enhancement]: It would be nice if the editor did support this, as described in #53548

[^cover-height]: This exact issue is discussed in #29705.

afercia commented 1 year ago

Oinging @aristath who's way more familiar with me with the styles engine implementation 🙏