How to Create Custom WordPress Editor Blocks in 2021

Read Time:12 Minute, 32 Second

The WordPress block editor (previously titled Gutenberg) includes a new way to add content to your WordPress posts, pages and soon all content on your WordPress site. It marks WordPress’ move into the page builder space.

The base set of default blocks is pretty robust, but what if you wanted to create your own custom block for laying out content? In the past you would use something like Advanced Custom Fields (ACF) or shortcodes. These days, custom blocks are where it’s at.

I’ve been working with React full-time for the past year, rebuilding WP Migrate DB Pro’s frontend. React is on my mind, and given that the WordPress editor is also written in React, I thought it would be a good idea to see what it would take to create a custom block. So let’s get into it!

Getting started

We’re going to go over what it takes to go from nothing to a relatively basic, custom Gutenberg block. The documentation for the block editor is still kind of all over the place but there is some decent information in the “Block Editor Handbook”. Unfortunately I didn’t find it all that easy to parse, so we’ll go over some of the steps I went through to get set up.

One thing I would recommend before diving head first into creating a Gutenberg block, for now at least, is to get a solid understanding of React and modern JavaScript. If you’re not familiar with things like JSX you could use the ES5 syntax, but you’d be best suited to use the modern syntax in the long run.

It’s fairly straightforward these days to get set up with the WP CLI ‘scaffold’ command. This command will set up a WordPress theme or plugin with a ‘blocks’ folder that contains the PHP and base CSS and JavaScript required to create a custom block. The only drawback that I noticed is that the JavaScript uses the old ES5 syntax rather than modern ESNext. Modern JavaScript allows us to write more concise code and use JSX in our custom block code.

You can also use the ‘create-guten-block’ tool by Ahmad Awais. It gives you a lot of the boilerplate stuff you need out of the box, like Webpack, ESNext support etc. Setting it up is fairly straightforward, and it’s similar to Create React App.

However, rather than using a third-party tool, I used one of the example custom blocks available in the gutenberg-examples repo on Github. The recipe card example covers a lot of what you’ll want in a minimally interactive custom block. It also includes @wordpress/scripts which is a package to run and build our JavaScript code, allowing us to use ‘ESNext’ syntax and JSX.

To get up and running, I just copied the recipe card example locally, modified the fields in the package.json file, and ran yarn.


Ok, so what is a ‘Block’ anyway? I had a hard time understanding this concept when I first started working with the block editor.

From the documentation:

Using a system of Blocks to compose and format content, the new block-based editor is designed to create rich, flexible layouts for websites and digital products. Content is created in the unit of blocks instead of freeform text with inserted media, embeds and Shortcodes…

Basically a ‘block’ is an organizational unit for editable ‘stuff’ in WordPress. At least that’s my take on it.

Blocks are the core building ‘block’, but how do you make one? Well, that part I can explain! Blocks are made almost entirely in JavaScript. Gutenberg brings a couple new actions (enqueue_block_assets and enqueue_block_editor_assets) to let you include your JavaScript and stylesheets to be used with Gutenberg.

function fancy_custom_block_block_init() { // automatically load dependencies and version $asset_file = include( plugin_dir_path( __FILE__ ) . 'build/index.asset.php'); wp_register_script( 'fancy-custom-block-block-editor', plugins_url( 'build/index.js', __FILE__ ), $asset_file['dependencies'], $asset_file['version'] ); wp_register_style( 'fancy-custom-block-block-editor', plugins_url( 'editor.css', __FILE__ ), array( ), filemtime( plugin_dir_path( __FILE__ ) . 'editor.css' ) ); wp_register_style( 'fancy-custom-block-block', plugins_url( 'style.css', __FILE__ ), array( ), filemtime( plugin_dir_path( __FILE__ ) . 'style.css' ) ); register_block_type( 'fancy-block-plugin/fancy-custom-block', array( 'editor_script' => 'fancy-custom-block-block-editor', 'editor_style' => 'fancy-custom-block-block-editor', 'style' => 'fancy-custom-block-block', ) );
} add_action( 'init', 'fancy_custom_block_block_init' );

All you essentially need to do PHP-wise is enqueue your JavaScript and CSS files and call register_block_type() with each asset’s handle.

A world of JavaScript

If you were thinking there’d be more PHP from this point, think again! From here on in, we’re in a JavaScript world.

The key aspect to creating blocks in Gutenberg is the registerBlockType() function. It does all the work.

registerBlockType( 'my-block/cool-block-name', { // ... Massive JS object

And that’s it! See you next time!

Ok so, there’s a bit more to it than that, but at its core, this is how to create a WordPress Gutenberg block. In the ‘Massive JS object’ there’s a few things to take note of.

If you take a look in /05-recipe-card-esnext/src/index.js file in the gutenberg-examples repo, you can see there’s a lot of config that goes into setting up a block. There are three main sections we’ll go over – attributes and the edit() and save() methods.


If you want a block that actually does something, like allow you to edit text, you have to use Gutenberg’s state management system. That’s what the attributes object is. It’s pretty much identical to React’s state management concept, a top level object that keeps track of properties and what’s changed.

attributes: { title: { type: "array", source: "children", selector: ".callout-title" }, mediaID: { type: "number" }, mediaURL: { type: "string", source: "attribute", selector: "img", attribute: "src" }, body: { type: "array", source: "children", selector: ".callout-body" }, alignment: { type: "string" }

The above is a JavaScript object that’s used to configure the appearance of the block.

You can see that for each editable ‘thing’ in your block you need to define an attribute for it. There’s the mediaID and mediaURL for the image, values for a title and body content, as well as the overall alignment of everything. The edit() method takes these attributes as an argument so we can modify them in the editor interface.


The edit() function lets you customize the editing interface in Gutenberg. If you’re familiar with React’s render() function it’s pretty similar. You essentially specify a return statement that has your JSX in it.

 edit: props => { const { className, isSelected, attributes: { mediaID, mediaURL, body, alignment, title }, setAttributes } = props; useEffect(() => { // console.log(props); }); const onChangeTitle = value => { setAttributes({ title: value }); }; const onSelectImage = media => { setAttributes({ mediaURL: media.url, mediaID: }); }; const onChangeBody = value => { setAttributes({ body: value }); }; const [imageClasses, textClasses, wrapClass] = sortOutCSSClasses( alignment || 'left', className ); return ( <> {isSelected && ( <BlockControls key="controls"> <AlignmentToolbar value={alignment} onChange={nextAlign => { setAttributes({ alignment: nextAlign }); }} /> </BlockControls> )} <div className={wrapClass} key="container"> <div className={imageClasses}> <div className="callout-image"> <MediaUpload onSelect={onSelectImage} type="image" value={mediaID} render={({ open }) => ( <Button className={mediaID ? "image-button" : "button button-large"} onClick={open} > {!mediaID ? __("Upload Image") : <img src={mediaURL} />} </Button> )} /> </div> </div> <div className={textClasses}> <RichText tagName="h2" className="callout-title" placeholder={__("Write a callout title…")} value={title} onChange={onChangeTitle} /> <RichText tagName="div" multiline="p" className="callout-body" placeholder={__("Write the callout body")} value={body} onChange={onChangeBody} /> </div> </div> </> );

You can see that the first thing we do is assign our attributes to some local variables to be used within the function.

const { className, isSelected, attributes: { mediaID, mediaURL, body, alignment, title }, setAttributes
} = props;

The above syntax is object destructuring and is mostly for the ‘less-typing-is-better’ effect. The next few functions are for handling the onChange() events in the editor. Basically when you change any value you want to update your application state. That’s what the setAttributes() method does.

const onSelectImage = media => { setAttributes( { mediaURL: media.url, mediaID:, } );

A lot of what’s included in the edit() method are event handlers that set state in the block’s attributes object. You’ll also notice the usage of the useEffect() hook in there. That’s a core React hooks method – under the hood Gutenberg is largely a wrapper over React.

The last part of the edit() function is the return statement. In here we have a bunch of JSX code that determines what the Gutenberg interface actually looks like. You can load in some standard components and blocks from the @wordpress/block-editor package.

import { RichText, MediaUpload, BlockControls, AlignmentToolbar
} from "@wordpress/block-editor";

Then in your return statement you can use them and assign attribute values:

 return ( ... <div className={textClasses}> <RichText tagName="h2" className="callout-title" placeholder={__("Write a callout title…")} value={title} onChange={onChangeTitle} /> <RichText tagName="div" multiline="p" className="callout-body" placeholder={__("Write the callout body")} value={body} onChange={onChangeBody} /> </div> ... );

The WordPress block editor includes a block called RichText. This is the main block you’ll want to use to make editable areas with. There are options for setting the type of HTML tag it outputs as well as a variety of other settings. The onChange attribute is where you assign your onChange() function. This is important as it’s how you update your block attributes and make everything update in real-time.


So that’s cool and all but we’re not actually saving anything yet. The next part of our custom block is the save() method.

This method defines how you want your block to be displayed on the front-end.

Gutenberg actually does something quite unique with how it saves data. Unless you’re saving data to post_meta the block configuration is serialized in an HTML comment above the block. WordPress appears to filter this out on the front-end, but it’s visible in the database record.

<!-- wp:fancy-block-plugin/fancy-custom-block {"mediaID":4035} -->
<div class="wp-block-fancy-block-plugin-fancy-custom-block bootstrap-block"><div class="wrap-left-"><div class="image-left-"><img class="the-image" src="http://wpdevelop.devtest/content/uploads/2020/01/image-5.jpg"/></div><div class="text-left-"><h2 class="callout-title">Test 2</h2><div class="callout-body"><p>Test</p></div></div></div></div>
<!-- /wp:fancy-block-plugin/fancy-custom-block -->

It’s a bit weird, but maintains backwards compatibility – if you ever disable Gutenberg the content will remain intact.

save: props => { const { className, attributes: { title, mediaURL, body, alignment } } = props; const [imageClasses, textClasses, wrapClass] = sortOutCSSClasses( alignment || "left", className ); return ( <div className="bootstrap-block"> <div className={wrapClass}> <div className={imageClasses}> {mediaURL && <img className="the-image" src={mediaURL} />} </div> </div> <div className={textClasses}> <RichText.Content tagName="h2" className="callout-title" value={title} /> <RichText.Content tagName="div" className="callout-body" value={body} /> </div> </div> );

The save() method is actually much simpler than the edit() method and you get the values of the attributes passed in as well. Then it’s mostly a matter of creating your front-end markup and inserting your values. Easy-peasy.

Issues I came across

Alright, so that’s the ‘how’ of creating a custom block, but what about the gotcha’s? Well, it turns out, there’s a few 😢.

The documentation is a little hard to parse. It’s not super easy to understand and is split up in a strange way. I found it hard to track down the documentation for each piece of building a custom block and ultimately just read through the example block code. There’s a lot of digging through code required to understand how things work.

I also found it really annoying working on a block that’s actively changing in code. Every time you reload Gutenberg, you’ll get the “This block appears to have been modified externally…” message because the markup of the block has changed.

I get why it’s throwing the error, but it slows you down.

I also had some strange console errors because SCRIPT_DEBUG was enabled. This constant is required to use the uncompiled version of React locally. Apparently this is a known issue, but it’s a bit crappy seeing console warnings because a WordPress core constant is enabled.

Console error

Further, React devtools are essentially useless as the React component names in Gutenberg are one character names…

React devtools screenshot

All this is to say: there are still some rough edges so your mileage may vary.


Ok, so it’s a bit of work to create a truly custom block. But what if you want to create a block type via a UI? Well, it turns out you can with ACF. ACF Blocks does much of this configuration all within the familiar ACF fields interface. The other benefit is that you can write the display code for ACF Blocks all in PHP.

ACF Blocks are a bit different than a custom created block though. The recommended approach with ACF Blocks is to edit the fields in the sidebar editor and view the preview in the main editor area. It’s a bit of a different experience to creating blocks, but it’s more inline with how ACF has traditionally worked – adding content in fields that are separate from the main editor.

If you’re only interested in rendering field values inside a block, this may be an option for you. You can also style the ACF block with an editor stylesheet so that it appears as it would on the frontend.

ACF Blocks feel more like a ‘hack’ of Gutenberg rather than the official way of creating blocks.

There are also other WordPress plugins that let you ‘assemble’ blocks, like Block Lab and Lazy Blocks. I haven’t tried either, but they may be an option if you’re looking for a minimal-code method for creating a custom block.

After it’s all said and done

So there you have it, how to create ‘custom’ blocks for the WordPress editor (or Gutenberg).

Have you created a custom Gutenberg block yet? What was your impression? Let us know in the comments.


You might also like this video