Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adopt a classes-first approach to CSS #54033

Open
cbirdsong opened this issue Aug 29, 2023 · 1 comment
Open

Adopt a classes-first approach to CSS #54033

cbirdsong opened this issue Aug 29, 2023 · 1 comment
Labels
CSS Styling Related to editor and front end styles, CSS-specific issues. [Type] Enhancement A suggestion for improvement.

Comments

@cbirdsong
Copy link

cbirdsong commented Aug 29, 2023

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.1

Injected CSS causes a number of problems for developers working on themes, plugins and even the editor itself. I'm not the first one2 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 styles3 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.4

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 block5:

.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 pattern6. 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 blocks7. 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:

  • Every type of layout: body .is-layout-<type>, specificity 0,1,1
  • Most(?) flex layouts: .wp-container-X.wp-container-X, specificity 0,2,0
  • Custom spacing rules on flow content: .wp-container-X.wp-container-X.wp-container-X.wp-container-X, specificity 0,4,0

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:

  • sites with a strict content security policy can restore full editor functionality by simply enqueueing these stylesheets
  • contributors and other developers can easily review the CSS the editor uses
  • the editor could just use this stylesheet instead of generating styles on the fly, which has been the source of issues like Container Style Element Breaks Expected CSS Selectors #36053

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:

.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:

.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:

  • 1,900 bytes
  • 13 rules
  • 36 selectors

The replacement CSS using semantic class names:

  • 863 bytes
  • 11 rules
  • 11 selectors

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:

  • the "full height" toggle on the cover block adding min-height: 100vh as an inline style
  • various typography settings adding stuff like text-transform: uppercase as an inline style

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:

  • Content width/wide width
  • Column width
  • Minimum height
  • Border widths
  • Border radius
  • Letter spacing
  • Line height

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:

  • has-spacing-custom
  • has-margin-block-custom
  • has-min-height-custom

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.

Footnotes

  1. I'm going to refer to both of these as "injected CSS" since "inline CSS" specifically refers to the style attribute.

  2. Notably: @afercia, @huubl, @markhowellsmead, @mrwweb, @langhenet, @daviedR, @dbushell

  3. 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.

  4. This exact issue is discussed in Inline style rule for full-height Cover Block in 5.7 can't easily be overridden #29705.

  5. It would be nice if the editor did support this, as described in Support multiple values in theme.json presets for progressive enhancement #53548

  6. 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;.

@jordesign jordesign added [Type] Enhancement A suggestion for improvement. CSS Styling Related to editor and front end styles, CSS-specific issues. labels Aug 29, 2023
@afercia
Copy link
Contributor

afercia commented Sep 4, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CSS Styling Related to editor and front end styles, CSS-specific issues. [Type] Enhancement A suggestion for improvement.
3 participants