Building your own WordPress Core Block CSS
In the first part of this series we’ve looked at how block CSS in WordPress works and is structured. We’ve then seen some limitations that come with that by looking at real world use cases. In this post we’ll see how to improve on the outlined issues by building our own version of core block CSS and replacing the default one from core with it.
If you haven’t read part one yet I highly recommend it before continuing. Just go and read it now, I’ll wait.
Done? Great! Let’s begin then.
Oh right, if you’re not interested in the process you can jump right to the end of this post to see the complete solution. But I’m telling you you’re missing out on an exciting journey!
Gutenberg and WordPress core
As some of you might know, the block editor, codename Gutenberg, is developed separately as a plugin and then regularly merged into WordPress core. The Gutenberg plugin itself also isn’t just this one, huge monolithic thing, but split into many small packages. Most of these can be used in separation outside of Gutenberg or WordPress.
Why am I telling you this? Well, the core block styles we’re talking about here are actually also a separate package. This package lives in the /packages/block-library
sub folder of the Gutenberg repository and is available via npm under the name of @wordpress/block-library
.
At a closer look I noticed that the core block styles are built from this package together with another package called @wordpress/base-styles
also maintained in the Gutenberg repository under /packages/base-styles
.
The Gutenberg repository also has a build script building all the core block styles.
So, great, I thought. We just take those two packages, run it through a build process inspired by the build script and get our own build of core block styles.
Yeah, well, it was not that easy…
What version?
As mentioned before at some point the core block styles from the Gutenberg repository get built from the two packages mentioned above and merged into WordPress. And then released as a new WordPress version.
If we want to replicate the exact CSS shipped with WordPress we need to build it from the exact same version of Gutenberg. So how do we figure out what that version is? Well that actually isn’t that difficult:
- Go to the development repository of WordPress: https://github.com/WordPress/wordpress-develop/
- Pick a version: https://github.com/WordPress/wordpress-develop/tree/5.7.0
- Open the package-lock.json that stores the exact versions of all packages used: https://github.com/WordPress/wordpress-develop/blob/5.7.0/package-lock.json
- Look for the versions of the two packages we’re interested in:
@wordpress/block-library
and@wordpress/base-styles
Great. What’s next? Right, building.
Building it
Okay so we create a new package.json
and install those two packages in the corresponding version. Then we need a SASS compiler like dart-sass to actually build the styles. Well, why not use the same version WordPress used and add it our package.json
.
Great. We now create a style.scss
SCSS file inspired by the build script we found earlier:
@import "@wordpress/base-styles/colors";
@import "@wordpress/base-styles/breakpoints";
@import "@wordpress/base-styles/variables";
@import "@wordpress/base-styles/mixins";
@import "@wordpress/base-styles/animations";
@import "@wordpress/base-styles/z-index";
@import "@wordpress/block-library/src/style";
and build it
sass --load-path=node_modules style.scss:style.css
Awesome. Done, right?
Just to be sure let’s compare the resulting file with the one included with WordPress core. Damn. There are differences. Let’s have a look.
Okay, source map comment. Let’s get rid of it:
sass --no-source-map --load-path=node_modules style.scss:style.raw.css
But there are still differences. Why?
PostCSS, @wordpress/postcss-plugins-preset,…
Oh, all CSS properties are prefixed! It seems Autoprefixer is applied here.
Okay, I could have seen that when looking at the build script. After building the CSS from the SCSS files the output is run through PostCSS. The configuration seems to be coming from another package called @wordpress/postcss-plugins-preset
which adds Autoprefixer and another package called postcss-custom-properties
that seems to add fallback values for CSS Variables (a.k.a. Custom Properties) for unsupported browsers. Okay.
So let’s install that. You already know the game. Get the version and install it, add PostCSS with the corresponding configuration and rebuild. (I am skipping how to add PostCSS and Autoprefixer for brevity).
Argh. Autoprefixer works and the differences got less but still not the same. And then it dawned on me…
Autoprefixer
The way Autoprefixer works is that you define a set of browsers to support and Autoprefixer then automatically decides what features need to be prefixed based on browser data from https://caniuse.com via the browserslist
package.
So two reasons why I still get differences building:
- I didn’t define a set of browser support, and WordPress doesn’t use the default settings.
- I am using todays caniuse data, but the files were built on some other day with data from that day.
Let’s fix this.
Issue 1: We get the configuration, this time from the package.json
file and add it to our own package.json
.
Issue 2: We get the version of the caniuse-lite
package and install it.
And rebuild. And still. no. match. 😠
Browserlist and caniuse.com data
So I started searching and remembered that even though I installed the right version of caniuse-lite
the way npm works is that there can be different versions of packages as sub dependencies of other packages. And so it was. Autoprefixer was having its own version with different data resulting in a different output.
Actually this is a problem specifically warned about in the documentation of browserslist. The proposed solution is to force update browser data for all packages. But since we want to reproduce a historic build this is not what we want.
So I looked into ways to force the same version of caniuse-lite
everywhere. No dice with npm it seems. But yarn has this nice feature called Selective dependency resolutions where you can force versions inside nested dependencies.
So if we add a resolutions field to our package.json
{
"resolutions": {
"caniuse-lite": "1.0.30001173"
}
}
and then install with yarn instead of npm
And rebuild.
And cross our fingers.
🤞
We finally managed to build an exact match for WordPress Version 5.7.0 style.css! 🥳
Other versions, other rules.
After I finished celebrating I thought: Hey, if it works for 5.7.0 it will surely work for other versions if I follow the same process, right?
And of course it didn’t. 😓
Apparently 5.7.0 is nice insofar as it really only uses one version of caniuse-lite
across all packages. Not so with 5.6.2. Across multiple packages multiple different versions are used. So the setup in the resolutions field is way more complex. Or maybe there is some other issue. I really don’t know at that point since I gave up. I manually compared the files and the differences were only minor differences with the prefixes so I think it’s good enough.
In the end, if you think about it, for real world usage it doesn’t really matter that much anyway. If you’re already rebuilding the styles it might even be a good idea to not have an exact copy of the files in WordPress core. The whole point of using Autoprefixer is to add prefixes according to the most recent data. So if our build deviates that is because it is actually better than the one in WordPress core.
The only issue that remains is that I’d still enjoy having a way to first rebuild an exact copy of the core styles to ensure that the source files are the right ones and only then start changing things. So if anybody digs deeper and finds the reason I’d be still curious to know but for now I’ve had enough of it.
What were we doing again?
So, we made a lot of effort until now trying to create a file that is an exact copy of a file we already have in WordPress core. That feels like … a great way to waste time. What were we trying to do actually?
Oh, right. We wanted to now take this build process, make minor changes to the source files and create our own version. Well, let’s do this then. Maybe see how we can satisfy the examples from the previous post.
Opinionated styles for some blocks
Let’s say we only want opinionated styles for the Pullquotes block. Easy now. We just pick from the original theme.scss
what we need.
@import "@wordpress/base-styles/colors";
@import "@wordpress/base-styles/breakpoints";
@import "@wordpress/base-styles/variables";
@import "@wordpress/base-styles/mixins";
@import "@wordpress/base-styles/animations";
@import "@wordpress/base-styles/z-index";
// This tag marks the start of the styles that apply to editing canvas contents and need to be manipulated when we resize the editor.
#start-resizable-editor-section {
display: none;
}
@import "@wordpress/block-library/src/pullquote/theme";
// This tag marks the end of the styles that apply to editing canvas contents and need to be manipulated when we resize the editor.
#end-resizable-editor-section {
display: none;
}
Disabling certain blocks
Same for removing structural styles for all blocks except heading, image and paragraph. We pick from the original style.scss
@import "@wordpress/base-styles/colors";
@import "@wordpress/base-styles/breakpoints";
@import "@wordpress/base-styles/variables";
@import "@wordpress/base-styles/mixins";
@import "@wordpress/base-styles/animations";
@import "@wordpress/base-styles/z-index";
// This tag marks the start of the styles that apply to editing canvas contents and need to be manipulated when we resize the editor.
#start-resizable-editor-section {
display: none;
}
@import "@wordpress/block-library/src/heading/style";
@import "@wordpress/block-library/src/image/style";
@import "@wordpress/block-library/src/paragraph/style";
@import "common.scss";
Overwriting core styles
In our example we tried to move the breakpoint for the columns block from 782px to 900px:
@import "@wordpress/base-styles/colors";
@import "@wordpress/base-styles/breakpoints";
$break-medium: 900px;
@import "@wordpress/base-styles/variables";
@import "@wordpress/base-styles/mixins";
@import "@wordpress/base-styles/animations";
@import "@wordpress/base-styles/z-index";
@import "@wordpress/block-library/src/style";
Aligning theme CSS with core CSS
And if we want to do something based on base styles we just import what we need in our own CSS file and work with it:
@import "@wordpress/base-styles/colors";
@import "@wordpress/base-styles/breakpoints";
@import "@wordpress/base-styles/variables";
@import "@wordpress/base-styles/mixins";
@import "@wordpress/base-styles/animations";
@import "@wordpress/base-styles/z-index";
.wp-block-column {
@include break-medium() {
&:not(:first-child) {
border-left: solid 2px black;
}
}
}
You see, instead of struggling to overwrite core block styles we now just create our own, clean, efficient, maintainable CSS that only contains what is actually needed.
Replacing core block styles with our own
We’ve now successfully created our own customized builds of the core block styles. All that remains to be done is actually replacing the core files.
As already mentioned in the previous post there are three files inside /wp-includes/css/dist/block-library
to replace. I am ammending the table from that post and adding some technical details:
Name | Front end | Editor | Handle | File |
---|---|---|---|---|
Structural Styles | Yes | Yes | wp-block-library |
style.css |
Opinionated Styles | Opt-in | Yes | wp-block-library-theme |
theme.css |
Editor Styles | No | Yes | wp-edit-blocks |
editor.css |
So we now need to unregister those handles at the right time and replace them with our own versions.
Actually I’ve already kind of covered how to do this for structural and opinionated styles in another post.
All that’s different is that instead of re-registering with an empty string we add our new URL:
<?php
add_action( 'enqueue_block_assets', function() {
wp_deregister_style( 'wp-block-library' );
wp_deregister_style( 'wp-block-library-theme' );
wp_register_style( 'wp-block-library', $url_to_our_new_style_css );
wp_register_style( 'wp-block-library-theme', $url_to_our_new_theme_css );
} );
?>
The one thing remaining now are the editor styles. This is a bit more tricky because of the way these styles are enqueued. Just unregistering and then re-registering like above is problematic because then we lose all the dependencies as well as RTL style definitions.
So you can either try to recreate all that when re-registering or we get a bit dirty and manipulate the internals of the already registered style.
I’ve decided to go down the second route. But please first make sure you’re understanding what you are about to do here because as I said this is a bit dirty and you’re messing with internal WordPress data bypassing the official APIs.
<?php
add_action('wp_default_styles', function (WP_Styles $wpStyles) {
$wpStyles->registered['wp-edit-blocks']->src = $url_to_our_new_editor_css;
});
?>
TL;DR
Awesome, we made it! We can now build our own versions of the core block styles, replace the default styles with them and finally completely bend block styles to our own will.
Let’s just summarize everything as well as welcome those people who jumped here straight from the introduction. 👋
For WordPress 5.7, to replace the structural styles CSS for core blocks you need the following things:
A style.scss
like this:
@import "@wordpress/base-styles/colors";
@import "@wordpress/base-styles/breakpoints";
@import "@wordpress/base-styles/variables";
@import "@wordpress/base-styles/mixins";
@import "@wordpress/base-styles/animations";
@import "@wordpress/base-styles/z-index";
@import "@wordpress/block-library/src/style";
a package.json
like this:
{
"browserslist": [
"> 1%",
"ie >= 11",
"last 1 Android versions",
"last 1 ChromeAndroid versions",
"last 2 Chrome versions",
"last 2 Firefox versions",
"last 2 Safari versions",
"last 2 iOS versions",
"last 2 Edge versions",
"last 2 Opera versions"
],
"dependencies": {
"@wordpress/block-library": "2.28.7",
"@wordpress/base-styles": "3.3.3",
"@wordpress/postcss-plugins-preset": "2.0.2"
},
"devDependencies": {
"browserslist": "4.16.1",
"caniuse-lite": "1.0.30001173",
"sass": "1.32.8"
},
"resolutions": {
"caniuse-lite": "1.0.30001173"
},
"scripts": {
"build": "sass --no-source-map --load-path=node_modules style.scss:style.css"
}
}
installed with yarn.
And some PHP code:
<?php
add_action( 'enqueue_block_assets', function() {
wp_deregister_style( 'wp-block-library' );
wp_register_style( 'wp-block-library', $url_to_our_new_style_css );
} );
?>
For those who did jump right here from the introduction I recommend reading the full post to understand what is happening as well as how to adapt this to your needs.
Final words
I hope this helps you to make theme development with Gutenberg more fun again! 🎈🥳🎈
I am currently working on a npm package that nicely encapsulates all of this so you don’t need to go digging for versions yourself. If that would be something you’d be interested in you may want to subscribe to my RSS feed or follow me on Twitter follow me on Mastodon to not miss any updates on the matter.