How to build custom blocks in your WordPress theme

By Ian Svoboda

If you’re just getting into block development, deciding where to build your custom blocks can be confusing. Much of the documentation from suggests using a plugin, but in the past you probably stored reusable components directly in your theme. In this blog post, I’ll break down why you probably want to store your custom blocks in your theme and how you can easily do it.

Blocks in themes vs plugins

Blocks can be created inside a plugin or inside of a WordPress theme. Which you choose depends on what the blocks are for and what they actually do.

If you’re building blocks that specifically relate to the design and functionality of the site you’re building, it would be best to build those in your theme. You won’t have to maintain a separate plugin and you can use your theme’s build process to build the block.

So if you’re just building blocks as part of the development of a theme, you almost definitely will be better off building the blocks inside your theme itself.

Desired Outcome

Once all of the below setup is completed the following things to be working:

  • New blocks added to includes/blocks will be seen automatically by WordPress.
  • All blocks will use the same build process as the theme.
  • All block assets will be automatically built without having to manually add a new entry1 each time.

So if you’re building a new block, you can just create a new folder for it, set it up, and our theme will just immediately start using it.

Let’s get going!

Setting up 10up-toolkit

10up-toolkit is a package that contains everything you need to build your static assets (JS/CSS/images) for your WordPress theme. You need this (or something like it) to build WordPress blocks and convert JSX into code the browser can understand.

If you haven’t set this up before, you can check out my article that explains how to set it up for the first time here.

If everything is working, you should be able to run npm run start or npm run build without errors.

Configuring 10up-toolkit for block files

If you’ve setup 10up-toolkit already, make sure you’ve enabled the option useBlockAssets in your package.json file under the 10up-toolkit key.

When you enable this option, 10up-toolkit will automatically process all of the scripts and stylesheets you specify in your block’s block.json file. This is great because you won’t have add a new entry every time you make a new block.

10up-toolkit will scan for block.json files in the blockDir folder (which by default is includes/blocks) then the block and it’s assets will be seen by 10up-toolkit.

Here’s an example of how the contents of the `10up-toolkit your package.json might look after this is added:

  "10up-toolkit": {
    "devURL": "https://your-local-site.test",
		"useBlockAssets": true, /* Add this */
    "entry": {
      "admin": "./assets/js/admin/admin.js",
      "blocks": "./includes/blocks/blocks.js",
      "frontend": "./assets/js/frontend/frontend.js",
      "core-block-overrides": "./includes/blocks/core-block-overrides.js",
      "editor-style": "./assets/css/editor-style.css"

Make sure to remove the comment on line 3 (regular JSON files don’t allow comments).

This article assumes you’re using the default paths/filenames from 10up-toolkit (along the lines of my previous article). So if you’ve customized these you’ll need to adjust the sample code accordingly.

Theme Setup

After configuring 10up-toolkit, you must incorporate a few elements into your theme to guarantee that blocks are automatically detected and ready for utilization. To achieve this, you’ll need to register any new block within WordPress using a function like register_block_type_from_metadata. This means that our solution needs to search for block.json files and register each one it locates within the theme.

10up actually has code for this included in their wp-scaffold repo. At 10up, wp-scaffold is used to start a new WordPress build and it includes 10up-toolkit in it and a bunch of other things they need.

That said, it’s not as simple to just use their code as is without modifying it some, so I’ve gone ahead and simplified the code a bit so it’s easier to drop into your theme.

This file uses PHP namespace to organize and scope our code. If you’re defining functions in a namespace like this code does, you need to tell PHP what namespace to use when you reference a named function. The $n function takes care of all of this for us so we can easily add our hooks and use the namespace correctly2.

A few key things are happening in the register_theme_blocks function:

  1. Check if the WordPress version is 6.0 or later and if not, temporarily add a filter to make sure the block asset URLs are determined correctly.
    • If your theme doesn’t support versions below 6 you could remove the related code, but I’d include it for backwards compatibility.
  2. Find every block.json file in the blocks directory includes/blocks and use those to determine the directory for each custom block.

Take the below snippet of code and copy it into a new file called blocks.php inside the includes folder in your theme:

 * Gutenberg Blocks setup
 * @package LWPTD
namespace LWPTD\Blocks;
 * Set up blocks
 * @return void
function setup() {
	$n = function( $function ) {
		return __NAMESPACE__ . "\\$function";
	add_action( 'init', $n( 'register_theme_blocks' ) );
	add_action( 'init', $n( 'register_block_pattern_categories' ) );
 * Automatically registers all blocks that are located within the includes/blocks directory
 * @return void
function register_theme_blocks() {
	global $wp_version;
	$block_dist_dir = get_template_directory() . '/dist/blocks/';
	$is_pre_wp_6 = version_compare( $wp_version, '6.0', '<' );
	if ( $is_pre_wp_6 ) {
		// Filter the plugins URL to allow us to have blocks in themes with linked assets. i.e editorScripts
		add_filter( 'plugins_url', __NAMESPACE__ . '\filter_plugins_url', 10, 2 );
	// Register all the blocks in the theme
	if ( file_exists( $block_dist_dir ) ) {
		$block_json_files = glob( $block_dist_dir . '*/block.json' );
		// auto register all blocks that were found.
		foreach ( $block_json_files as $filename ) {
			$block_folder = dirname( $filename );
			register_block_type_from_metadata( $block_folder );
	if ( $is_pre_wp_6 ) {
		// Remove the filter after we register the blocks
		remove_filter( 'plugins_url', __NAMESPACE__ . '\filter_plugins_url', 10, 2 );
 * Filter the plugins_url to allow us to use assets from theme.
 * @param string $url  The plugins url
 * @param string $path The path to the asset.
 * @return string The overridden url to the block asset.
function filter_plugins_url( $url, $path ) {
	$file = preg_replace( '/\.\.\//', '', $path );
	return trailingslashit( get_stylesheet_directory_uri() ) . $file;
 * Register block pattern categories
 * @see
 * @return void
function register_block_pattern_categories() {
	// Register a block pattern category
		[ 'label' => __( 'Theme Blocks', 'lwptd' ) ]

Once you’ve created the file, require it in your functions.php file:

<?php //functions.php
// Other requires here....
require_once get_template_directory() . '/includes/blocks.php';

Creating a block

Now let’s create a new block to test out this functionality and make sure it all works. The fastest and simplest way to create new blocks is using the @wordpress/create-block package via npx. This will automatically create the files you need so you just need to tweak block.json a bit and you’re off and running.

Here we will use the create-block package to create an example dynamic block. Dynamic blocks use PHP to render on the frontend and only store child blocks (if they have any) in the database.

Dynamic blocks will update automatically in all locations where you use them, which is typically the desired outcome in theme development. Any code changes you make to the block will show up immediately for any existing instance of the block.

Open your terminal and navigate to includes/blocks. Copy and paste the below command into your terminal and run it to create a new dynamic block. You can replace the namespace and block name (example-block) with anything else you’d prefer.

 npx @wordpress/create-block@latest --variant dynamic --no-plugin --namespace lwptd example-block

Here’s a quick explanation about each option:

  • --variant dynamic This creates a dynamic block instead of a static block.
  • --no-plugin Excludes any files needed for a WordPress plugin.
  • --namespace All blocks must have a namespace to prevent conflicts with other blocks. You might think of this as a way of saying “all of these blocks go together” or come from the same source.
  • example-block This is the block name for the command, which can be any kebab-case string. Feel free to change this to whatever you want.

Once you run the above command, WP CLI will create the block in a new folder that matches the block name in the current folder in your terminal.

The generated block.json file currently has 2 typos in the style and editorStyle keys. Both of these will point to CSS files that don’t exist (style-index.css, index.css). Double check the generated block.json file and make sure those values are files that actually exist (style.scss, editor.scss)

Now you have a new dynamic block that uses PHP for rendering on the frontend and be all set up to process the block’s JS and SCSS files using 10up-toolkit. When you create a new block and place it inside includes/blocks it will automatically be seen by WordPress and 10up-toolkit.

  1. Entry refers to a single JavaScript or CSS file that will be processed by webpack. ↩︎
  2. If you did this by hand you’d need to prepend __NAMESPACE__ in front of each named function, e.g.: add_action('init', __NAMESPACE__ . '\\register_theme_blocks');. ↩︎

Get my free 4-part WordPress hooks mini course

Sent straight to your inbox.