Building your own WordPress Core Block CSS

Estimated Reading Time: 11min

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:

  1. Go to the development repository of WordPress: https://github.com/WordPress/wordpress-develop/
  2. Pick a version: https://github.com/WordPress/wordpress-develop/tree/5.7.0
  3. 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
  4. 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:

  1. I didn’t define a set of browser support, and WordPress doesn’t use the default settings.
  2. 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 to not miss any updates on the matter.