WordPress.org

WordPress Developer Blog

Beyond block styles, part 1: using the WordPress scripts package with themes

Tell me if you’ve run into this problem before: you build an awesome block style for one of the multitudes of blocks that WordPress offers. Maybe it’s something like a gradient ring for the Image block:

Then you create another style for a tape effect. And another that adds a sepia filter—you want some of those images to have an old-school feel.

Pretty soon you realize you could mix and match a few of them to create unique combinations.

But there’s just one problem standing in your way: WordPress only lets you apply one style to a block at a time.

To solve this problem, you could easily create some extra styles that combined them. Who wouldn’t want to try out that block style named Gradient Ring with Tape and Sepia Filter?

Yeah, that’s a slippery slope. Before you know it, you have dozens of block styles to manage, each combination adding more overhead and cluttering the Styles panel. I wouldn’t wish that headache on my worst enemy.

There must be a better way…

In this series, we are going to take a journey into the unknown—or at least, the undocumented—far beyond the confines of the Block Styles API. It’s going to be long and maybe a little rocky, but you and I will come out on the other side unscathed—and with several new tricks to make styling magic with our themes.

The goal of this series is to teach you how to work with the APIs that block developers get to play with every day. Only, you’ll use those features in a theme, instead of a block. And you’ll use code and techniques that conform to the guidelines for the WordPress theme directory.

So buckle up. Let’s get going!

What are we building?

Block styles are great when you want to build a one-off design option into the UI. They are easy to register in PHP with register_block_style() or JavaScript with registerBlockStyle(). Add a few lines of CSS and you have a brand-new style.

But let’s be blunt. The Styles UI panel is limited. It’s a list of buttons. Their names get cut off after a few characters. And you have to hover over them to see a preview:

I mean, I’m a developer. I want more. I want to go beyond the limited user experience the block styles system offers.

What if those styles had specific controls that were custom-tailored for the use case? For example, instead of the gradient ring style mentioned above, you let your theme users choose from any defined gradient? Or, instead of a sepia filter, you had custom controls that opened up the entire CSS filter specification?

You can do all those things—and a whole lot more.

In this tutorial series, you will learn how to extend the Separator block with an emoji icon picker like this:

The icon picker is just to keep things fun while you learn the basics of building this control. When you have that down, you will be able to do whatever you want with it.

Prerequisites

I won’t sugarcoat this: there is a skill jump from being able to build a basic theme with minimal programming to being able to make the editor do your bidding. And there’s no way I could cover all the foundational knowledge that you need in this post.

Before moving forward, you need to be familiar with:

  • Node and npm: You must have Node.js and npm installed on your local machine to follow along.
  • JavaScript programming: You should at least have the foundational knowledge necessary to read, write, and understand intermediate JavaScript.
  • Block development: This series will walk you through the necessary bits to get started, but you’ll need to be comfortable enough to expand beyond this for custom features.
  • Theme development: In general, you should know the basics of developing a WordPress theme, particularly working with PHP.
  • CSS/SCSS: You’re going to write some code  in .scss files, which is readily transferable to vanilla CSS.
  • Build tools: For the most part, you should be familiar with running commands from a Command Line Interface (CLI), such as npm run <command>. A basic understanding of webpack would be useful, too.

All code examples in this tutorial series have been tested in a direct fork of the default Twenty Twenty-Three theme. You’ll have the best experience creating your own fork and following along, but feel free to work from another block theme if you want.

Setting up your theme to use the WordPress scripts package

The most important piece of getting your theme to talk to the editor is defining your theme’s assets structure and importing the packages you need for building with the editor.

It’s entirely possible to write vanilla JavaScript or use a custom system. I’ve been down that road, and it was not fun. Your life will be much simpler when you use the pre-built tools WordPress offers.

Creating the initial files and folders to house assets

Naming things is hard. No doubt you have your own naming scheme for files and folders. But for this tutorial to work, you need to use mine.

With that in mind, there will be two top-level folders that house assets for the theme:

  • resources: The development files where you write CSS and JavaScript code.
  • public: The production files after they’ve run through the build process.

After this tutorial, you can use your own system. You’ll want to make sure you change the file references.

Now make a few blank CSS and JavaScript files inside of the /resources folder with the below names. Your structure should look like this:

  • resources
    • js
      • editor.js
    • scss
      • screen.scss
      • editor.scss

Don’t worry about creating the /public folder. The build process will take care of that.

The scripts package renames style.scss to style-index.css during the build process, which is why screen.scss was chosen for the primary stylesheet name.

Installing the WordPress scripts package

Originally, the @wordpress/scripts package was created to simplify the block-building process. Today, it is flexible enough to work with themes too. Even if you’ve never built a block, you can have a lot of fun with this.

Create a package.json file in your theme and add this code to it:

{
	"name": "your-project-name",
	"scripts": {
		"start": "wp-scripts start --webpack-src-dir=resources --output-path=public",
		"build": "wp-scripts build --webpack-src-dir=resources --output-path=public"
	}
}

The start and build commands under scripts are just two of the available scripts you can use. You should definitely dive into the others and use them. For this tutorial, these two are all we need to get the build process going and to define custom source and output directories.

If you don’t define --webpack-src-dir and --output-path, they will default to the /src and /build folders, respectively.

Now open your command line interface and navigate to your theme’s directory. Enter this command to install the necessary packages:

npm install @wordpress/scripts path webpack-remove-empty-scripts --save-dev

The @wordpress/scripts package is the foundation for working with the block editor and its components. It also gives us a lot of useful tools without a lot of work. The extra path and webpack-remove-empty-scripts packages are helpers you’ll use later, in your webpack configuration.

Once you’ve executed the npm install command with the --save-dev flag, your package.json should now include those three packages as dev dependencies:

{
	"name": "your-project-name",
	"scripts": {
		"start": "wp-scripts start --webpack-src-dir=resources --output-path=public",
		"build": "wp-scripts build --webpack-src-dir=resources --output-path=public"
	},
	"devDependencies": {
		"@wordpress/scripts": "^26.8.0",
		"path": "^0.12.7",
		"webpack-remove-empty-scripts": "^1.0.3"
	}
}

Theme webpack configuration

If the thought of configuring webpack sounds frightening, don’t worry. I’ve been there—still there on most days if I’m being perfectly transparent. Most of this is boilerplate stuff you can copy, paste, and make slight alterations to.

The most important piece of this is adding your custom entry points: the locations of your CSS and JS files.

Now create a new webpack.config.js file in your theme and add this code to it:

// WordPress webpack config.
const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );

// Plugins.
const RemoveEmptyScriptsPlugin = require( 'webpack-remove-empty-scripts' );

// Utilities.
const path = require( 'path' );

// Add any a new entry point by extending the webpack config.
module.exports = {
	...defaultConfig,
	...{
		entry: {
			'js/editor':  path.resolve( process.cwd(), 'resources/js',   'editor.js'   ),
			'css/screen': path.resolve( process.cwd(), 'resources/scss', 'screen.scss' ),
			'css/editor': path.resolve( process.cwd(), 'resources/scss', 'editor.scss' ),
		},
		plugins: [
			// Include WP's plugin config.
			...defaultConfig.plugins,

			// Removes the empty `.js` files generated by webpack but
			// sets it after WP has generated its `*.asset.php` file.
			new RemoveEmptyScriptsPlugin( {
				stage: RemoveEmptyScriptsPlugin.STAGE_AFTER_PROCESS_PLUGINS
			} )
		]
	}
};

This is already set up with the CSS and JS file paths you defined earlier, so there is really nothing else you need to do, unless you plan to customize it.

You should have also noticed that the code uses the path package for resolving some paths. And the webpack-remove-empty-scripts plugin does a little cleanup at the end. Essentially, it removes empty JS files that would otherwise be generated for your stylesheets.

Using the scripts package

At this point, you have all the complex setup finished. Now let’s test this thing and make sure it works.

Open your command line program on your computer and run the start command from your theme’s directory:

npm run start

This will build out the development versions of your files under the /public folder, which should look like this:

  • public
    • js
      • editor.js
    • css
      • screen.css
      • editor.css

All nice, neat, and orderly, the way we theme authors like things, right? 

The start command will also keep you in “watch” mode. You can make any code changes directly to your asset files under /resources, and they will be built with every file save. You can switch out of this mode by hitting Ctrl + C on your keyboard.

The build script that you added to package.json will build the production versions of your files. Go ahead and give it a go:

npm run build

Of course, you don’t have any code yet, so you’re not actually building anything but empty files. But we’ll fix that soon enough.

Loading scripts and styles

If you’re reading this tutorial, you should already know how to load scripts and styles in WordPress. But there are a few things you’ll want to change, so don’t skip out on this part.

One of the benefits of using @wordpress/scripts is that it automatically creates a {filename}.asset.php file for every entry point defined in webpack.config.js. This file does two important things:

  • Creates an array of dependencies, so you don’t have to manually define them for your script/style calls.
  • Defines a version for cache busting.

That’s a pretty sweet deal and makes managing scripts and styles much easier. 

Open your theme’s functions.php file (create one if it doesn’t exist). There are three different hooks that you need to enqueue scripts and styles on. 

First, enqueue your theme’s main stylesheet (called screen.css) on the front end of your site:

add_action( 'wp_enqueue_scripts', 'themeslug_assets' );

function themeslug_assets() {
	$asset = include get_parent_theme_file_path( 'public/css/screen.asset.php' );

	wp_enqueue_style(
		'themeslug-style',
		get_parent_theme_file_uri( 'public/css/screen.css' ),
		$asset['dependencies'],
		$asset['version']
	);
}

First, the code assigns the array returned from the screen.asset.php file to an $asset variable. Then, it calls the normal wp_enqueue_style() function to enqueue the stylesheet. Note how the dependencies and version are passed directly to the function.

You must also register the same stylesheet with the editor if you want your styles to work on the backend. You do this with the add_editor_style() function (you cannot pass dependencies or a version to this):

add_action( 'after_setup_theme', 'themeslug_editor_styles' );

function themeslug_editor_styles() {
	add_editor_style( [
		get_parent_theme_file_uri( 'public/css/screen.css' )
	] );
}

Finally, you need to enqueue your editor script and style:

add_action( 'enqueue_block_editor_assets', 'themeslug_editor_assets' );

function themeslug_editor_assets() {
	$script_asset = include get_parent_theme_file_path( 'public/js/editor.asset.php'  );
	$style_asset  = include get_parent_theme_file_path( 'public/css/editor.asset.php' );

	wp_enqueue_script(
		'themeslug-editor',
		get_parent_theme_file_uri( 'public/js/editor.js' ),
		$script_asset['dependencies'],
		$script_asset['version'],
		true
	);

	wp_enqueue_style(
		'themeslug-editor',
		get_parent_theme_file_uri( 'public/css/editor.css' ),
		$style_asset['dependencies'],
		$style_asset['version']
	);
}

Now you’re all set up to start tinkering with the block editor. It’s going to be fun!

Stay tuned for the next post

This feels like a natural stopping point for Part 1 of the series. You and I have covered a lot of ground together. We haven’t actually built anything yet, but we’re well-prepared for the next steps.

I’m usually not one for assigning homework, but take some time to play around with this setup. Try your hand at writing some basic CSS or JavaScript and just getting a feel for how everything works. Then take a few minutes to read the @wordpress/scripts documentation.

In Part 2, you’ll learn to design a set of custom block styles. Part 3 will bring it all together into a beautiful and functional editor control. From there, well, you’ll be building some amazing features.

Resources

Props to @juanmaguitar, @marybaum, and @bph for feedback and review. Mountains photo by @annezazu from the WordPress photo directory.

8 responses to “Beyond block styles, part 1: using the WordPress scripts package with themes”

  1. Bridget Avatar
    Bridget

    Great article! I’m looking forward to the rest in the series.

    Can you help me understand why you used add_editor_style() to enqueue screen.css in the editor, and not enqueue_block_assets() – to enqueue screen.css on both editor and front-end?

    Do you not need the dependencies from public/css/screen.asset.php in the editor?

    The docs say add_editor_style() is a callback for custom TinyMCE editor stylesheets, which doubly confuses me on using it for block stuff.

    I have learned a lot from your many WordPress developer focused articles over the years Justin. Thank you!!!

    1. Justin Tadlock Avatar

      That is a great question! add_editor_style() is the primary function for loading theme stylesheets in the editor (for both block and classic themes). With block themes, it will auto-prefix your CSS selectors with the .editor-styles-wrapper class and output the code inline. This prefix is the often necessary to win CSS specificity battles in the editor.

      You are correct that the function doesn’t support dependencies. In this particular case, I knew that there were no dependencies for screen.css, so it’s a non-issue.

      In most projects (at least that I’ve seen), theme stylesheets wouldn’t have dependencies. Although, that is not the case 100% of the time.

      If there are dependencies, using the enqueue_block_assets hook along with the wp_enqueue_style() function might be a better choice. But you lose out on the .editor-styles-wrapper prefix when going this route.

      There is another safe route in some cases with add_editor_style() for stylesheets with dependencies. If all of the dependent stylesheets are under your control (e.g., various theme stylesheets), you could pass all of them into add_editor_style() too.

      I hope that helps. We definitely need a definitive documentation page on enqueuing scripts/styles. There’s a lot of nuance that’s easy to overlook when writing tutorials here on the blog.

  2. Christophe Avatar
    Christophe

    Hey there!

    Looking forward for the next steps of this guide.
    I am also in the process of setting up a skeleton theme using the new block theme and native blocks with react.

    I do however have a question in regards of the dependencies within the asset.php file, how do you make a file to be a dependency within the JS?\

    And on another note how can you use WordPress jQuery library instead of importing to your own JS file where it just make the file much larger. I tried setting the the “jQuery” as an external but the linting still says as undefined.

    And finally I would like to sue the wp-scripts start to always lint before building either in dev or production so that coding standards are applied.

    1. Justin Tadlock Avatar

      If I have an extra dependency that I needed to add, I’d just merge it in the wp_enqueue_script() call. For example, if was adding jQuery, I’d probably just change $script_asset['version'] would become array_merge( $script_asset['version'], array( 'jquery' ) ). There might be a way to do that with the scripts package, but this method would work fine.

  3. Tim Fetter Avatar

    This is just what I was looking for. Thank you.
    I noticed that when I run start, a map is generated for the front-end.css, but running build does not. Have you seen this behavior before or know how to generate the map?

    1. Justin Tadlock Avatar

      Source maps are generated in development but are not in build (that’s usually not something included in the production build). But I’m sure it’s possible to overwrite this in your webpack config. Here’s the link to the webpack docs if you want to dive a bit deeper.

  4. Peter Westerlund Avatar

    The dependencies that are added to the array in the assets php file, where are they taken from? Like, where do I define which dependencies are needed?

    1. Justin Tadlock Avatar

      The dependencies are automatically generated via the scripts package. So, if you import a dependency in your editor.js file, for example, the scripts package will add that to the generated editor.asset.php file. You will see more of this in parts 2 and 3 of this series.

Leave a Reply

Your email address will not be published. Required fields are marked *