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:
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]:
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:
Every type of layout: body .is-layout-<type>, specificity 0,1,1
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)
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 #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:
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.
[^terminology]: I'm going to refer to both of these as "injected CSS" since "inline CSS" specifically refers to the style attribute.
[^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.
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: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: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]:
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:
clamp()
values for custom font sizes with responsive typography (global browser support: 93.62%)margin-block-start
(global browser support: 91.49%)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:
body .is-layout-<type>
, specificity0,1,1
.wp-container-X.wp-container-X
, specificity0,2,0
.wp-container-X.wp-container-X.wp-container-X.wp-container-X
, specificity0,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/gOyXzYWWhat 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-
classesAs 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:
min-height: 100vh
as an inline styletext-transform: uppercase
as an inline styleThese should all be replaced with equivalent classes like
is-full-height
orhas-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 like
has-background
andhas-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.
[^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
andis-content-justification-center
, but those classes are unused and injected CSS is still generated to add the requiredflex-wrap: nowrap;
andjustify-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.