The WordPress block editor was made the default editor for WordPress in December 2018. Adoption may have been slow at first, but the pace of development has increased exponentially. Today, custom blocks are at the core of extending WordPress. In this article, we’ll cover two ways to create custom blocks: with React and the @wordpress/create-block
tool, and with the ACF Blocks feature of Advanced Custom Fields.
What Are WordPress Blocks?
WordPress blocks are components used for creating and editing elements in a WordPress post or page. Native WordPress blocks split into static blocks, rendered in the browser, and dynamic blocks, which are rendered on the server.
Content and markup from static blocks are saved in the post content, and manual edits are required to change the content. Static blocks are written in JavaScript.
Dynamic blocks, as the name implies, can change their content without the manual intervention required by static blocks. For example, a dynamic block could be used to display the site’s name, picking up the name from the site’s settings when the block is rendered. Dynamic blocks use JavaScript to create the editor experience, and PHP to render the front end markup on the server.
ACF Blocks are a custom type of dynamic block. Even when they’re used in the same way as a static WordPress block, they’re rendered by ACF on the server and allow you to use PHP logic.
Creating Blocks With React
The Block Editor Handbook is a great resource when it comes to actually creating blocks, but it may not be the best starting point. Go ahead and dive right in if you have a solid understanding of React and modern JavaScript, but if not, you’ll probably want to go over the React Quick Start tutorial.
WordPress blocks have come a long way since their introduction in WordPress 5.0. There’s even an officially supported tool, @wordpress/create-block
, for scaffolding a WordPress plugin that registers a block. The package generates everything you need to get started: PHP, JavaScript, and CSS.
Setting Up Your Dev Environment
The first thing you’ll need is to set up your development environment. The WordPress documentation on block development environments goes into more detail, but basically there are three things you need:
We recommend using Local to create your local WordPress install, or wp-env, which is maintained by the WordPress project.
Once you have that in place, you’re ready to create your first block. Open your terminal and navigate to the directory where your local WordPress install stores its plugins. If you’re using Local, this will be something like Local Sites\mywp\app\public\wp-content\plugins
. Using wp-env
will map any directory you choose to work from as the plugin directory for your site.
Once you’re in the right directory, run the following line:
npx @wordpress/create-block
This calls the package, and then asks you a series of questions to help customize your plugin. The first choice you need to make is if it’s going to be static or dynamic. I want to keep this simple, so I’m going to choose “static.”
Let's customize your WordPress plugin with blocks:
? The template variant to use for this block: static
? The block slug used for identification (also the output folder name): mediummike
? The internal namespace for the block name (something unique for your products): mediummike-basicblock
? The display title for your block: Medium Mike's Basic Block
? The short description for your block (optional): A block that displays a text message.
? The dashicon to make it easier to identify your block (optional): welcome-view-site
? The category name to help users browse and discover your block: widgets
? Do you want to customize the WordPress plugin? Yes
? The home page of the plugin (optional). Unique URL outside of WordPress.org:
? The current version number of the plugin: 0.1.0
? The name of the plugin author (optional). Multiple authors may be listed using commas: Mike Davey
? The short name of the plugin’s license (optional): GPL-2.0-or-later
? A link to the full text of the license (optional): https://www.gnu.org/licenses/gpl-2.0.html
? A custom domain path for the translations (optional):
? A custom update URI for the plugin (optional):
You’ll also need to choose a block slug (which will also be used as the name of the folder) and provide an internal namespace. Some care should be taken here to use a namespace that won’t cause namespace clashes, uses a scalable structure so it can be extended if needed, and only uses lowercase alphanumeric characters or dashes.
Next enter the display title for your block, and provide a short description. You can also choose a dashicon. This is completely optional, but I elected to use welcome-view-site
, because it reminds me of the Paranoia roleplaying game.
You must also decide on a category for your block. This won’t provide your block with any functionality, but it helps make it easier to discover.
Alternatively, you can skip the interactive mode and scaffold your plugin with a more complex command:
$ npx @wordpress/create-block@latest [options] [slug]
Using slug
triggers quick mode, meaning whatever you put here is used as the block slug, the output folder name for your scaffolded files, and the name of the plugin itself. Using options
allows args that change the default configuration.
At this point, you should have a working WordPress plugin installed on your local WordPress site. Pop into your “Plugins” page and confirm that it’s installed. You can also activate it at this stage, but of course it doesn’t do anything yet. To add functionality, we’ll need to dive into the scaffolded files.
Block Plugin File Structure
The scaffolded plugin should have some files in its root, and three subdirectories:
├── yourplugin
├── build
├── node_modules
├── src
├── .editorconfig
├── .gitignore
├── mediummikesbasicblock.php
└── src
├── app.js
├── models.js
├── routes.js
└── utils
├── another.js
├── constants.js
└── index.js
There are two files in the root we’re concerned with here: package.json
and a PHP file with the same name as your slug.
The package.json
file is where properties and scripts are defined. Your scaffolded block should have one dependency initially:
"devDependencies": {
"@wordpress/scripts": "26.16.0"
This bundles the tools and configurations you’ll need to build WordPress blocks. You can install other dependencies if you wish, but you get a lot of what you’ll need right here.
The scripts
section of package.json
defines the commands available to npm run ()
:
"scripts": {
"build": "wp-scripts build",
"format": "wp-scripts format",
"lint:css": "wp-scripts lint-style",
"lint:js": "wp-scripts lint-js",
"packages-update": "wp-scripts packages-update",
"plugin-zip": "wp-scripts plugin-zip",
"start": "wp-scripts start"
The main scripts here are build
and start
. Using npm run build
will create a production build with compressed code. This speeds up downloads, but it’s harder to read and debug.
Use npm run start
during the development phase to create an uncompressed build of the code and ease the development process. This command also starts a watch process to rebuild the file each time it’s saved.
These scripts need a JavaScript file to build, and default to looking at src/index.js
in the scaffolded plugin, and saving the built file to build/index.js
.
No matter which build script is used, the block won’t run in the editor if WordPress doesn’t know about it. The PHP template file generated by npx @wordpress/create-block
includes an init
action to register the block and specify the editor script handle registered from the metadata provided in build/block.json
with the editorScript
field. When the editor loads, it will load this script and register the block.
/**
* Registers the block using the metadata loaded from the `block.json` file.
* Behind the scenes, it registers also all assets so they can be enqueued
* through the block editor in the corresponding context.
*
* @see https://developer.wordpress.org/reference/functions/register_block_type/
*/
function mediummike_mediummike_block_init() {
register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'mediummike_mediummike_block_init' );
WordPress blocks are essentially JSON objects. The JavaScript that defines that object is located in the src/index.js
file.
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
/**
* Internal dependencies
*/
import Edit from './edit';
import save from './save';
import metadata from './block.json';
registerBlockType( metadata.name, {
/**
* @see ./edit.js
*/
edit: Edit,
/**
* @see ./save.js
*/
save,
} );
The registerBlockType
function is imported from @wordpress/blocks
and takes two arguments: a block name and a block configuration object. The block configuration object contains information about the block, such as its title, description, and category.
The src/index.js
file is the entry point for the block, and it’s where you’ll define the registerBlockType
function and import any necessary dependencies. The scaffolded file will have some dependencies already set up:
import './style.scss';
: This lets webpack process any CSS, SASS, and SCSS files referenced in the block, bundling together any files that contain thestyle
keyword. Code used is applied to the block on both the front end of the site and in the editor.import Edit from './edit';
: This imports the edit function for the block, which is defined in theedit.js
file. Theedit.js
file contains the code that will be executed when the user edits the block in the WordPress editor. It typically includes the block’s edit form, which allows the user to set the block’s attributes.import save from './save';
: This imports the save function for the block, which is defined in thesave.js
file. Thesave.js
file contains the code that will be executed when the user saves the block in the WordPress editor. It typically includes the code that will be executed when the block is saved, such as saving the block’s attributes to the database.import metadata from './block.json';
: This imports the metadata for the block, which is defined in theblock.json
file.
Block Metadata in block.json
The block.json
file contains information about the block, such as its name, title, and description. It also contains information about the block’s attributes, which are the properties that can be set for the block.
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "mediummike-basicblock/mediummike",
"version": "0.1.0",
"title": "Medium Mike's Basic Block",
"category": "widgets",
"icon": "welcome-view-site",
"description": "A block that displays a text message.",
"example": {},
"supports": {
"html": false
},
"textdomain": "mediummike",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"viewScript": "file:./view.js"
}
Let’s unpack some of the block metadata and see what it’s doing.
supports
: This property defines the features that the block supports. For example, if the block supportsalign
, it means that the block can be aligned to the left, right, or center. If the block supportsanchor
, it means that the block can be linked to using an anchor tag.textdomain
: This property defines the text domain for the block. The text domain is used to translate the block’s text into different languages.editorScript
: This property defines the JavaScript file that will be used to render the block in the WordPress editor. The file should be located in thesrc
directory of the block.editorStyle
: This property defines the CSS file that will be used to style the block in the WordPress editor. The file should be located in thesrc
directory of the block.style
: This property defines the CSS file that will be used to style the block on the front-end of the website. The file should be located in thesrc
directory of the block.viewScript
: This property defines the JavaScript file that will be used to render the block on the front-end of the website. The file should be located in thesrc
directory of the block.
Setting Block Attributes
Attributes define the block’s properties and behavior. The scaffolded block.json
file doesn’t have any attributes to begin with. 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.
To set attributes in block.json
, add a new object called attributes
to the file at the same level as the name
and title
fields. Individual attributes are defined within the object by key-value pairs, with the key naming the attribute and the value providing the definition.
"attributes": {
"message": {
"type": "string",
"source": "text",
"selector": "div",
"default": ""
}
},
Attributes must be defined for each editable part of the block. The example above allows a content editor to add a message that will be displayed when the post is published.
At minimum, the attribute definition must contain either a type
, indicating the type of data stored, or an enum
with an array of allowed values. It is possible to use type
and enum
together to first indicate the type of data, and then define it from a set of fixed values.
The source
field defines how attribute values are extracted from saved post content. Basically it’s a way to map HTML post markup into a JavaScript form. If you don’t define it, the data will be stored in the comment delimiter. Specifying a selector
argument will run it against the block’s matching elements, in this case div
.
There is no limit to the number of attributes that can be added. For example, we could add attributes that deal with the post’s images:
"mediaID: {
"type": "number"
},
"mediaURL: {
"type": "string",
"source": "attribute",
"selector": "img",
"attribute": "src"
},
In some cases, like mediaID
above, the type
is all you need. The mediaURL
in the example above needs more: source: "attribute,
to specify that the data is stored in an HTML element attribute, with selector: "img",
indicating it should match the img
element. Finally, the attribute
specified by source
must be defined, in this case with src
, i.e., the URL associated with the HTML src
element.
Edit and Save
The attributes
set in src/block.json
are passed to the edit()
and save()
functions defined in src/edit.js
and src/edit.js
, along with additional parameters. The src/edit.js
file also needs to have the setAttributes
function to update the attributes.
The src/edit.js
file also includes component imports to build and modify the block’s UI. For example, you can add a button component by importing an existing component from `@wordpress/components:
import { Button } from '@wordpress/components';
export default function MyButton() {
return <Button>Clickety-click!</Button>;
}
One of the more commonly used components is TextControl, useful for allowing users to enter and edit short amounts of text. For longer text, TextareaControl is probably a better option.
Code comments on the scaffolded edit.js
and save.js
files are pretty thorough, with a lot of hints about how to modify the files. Here’s the scaffolded edit.js
with no modifications:
/**
* Retrieves the translation of text.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-i18n/
*/
import { __ } from '@wordpress/i18n';
/**
* React hook that is used to mark the block wrapper element.
* It provides all the necessary props like the class name.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops
*/
import { useBlockProps } from '@wordpress/block-editor';
/**
* Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
* Those files can contain any CSS code that gets applied to the editor.
*
* @see https://www.npmjs.com/package/@wordpress/scripts#using-css
*/
import './editor.scss';
/**
* The edit function describes the structure of your block in the context of the
* editor. This represents what the editor will render when the block is used.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit
*
* @return {Element} Element to render.
*/
export default function Edit() {
return (
<p { ...useBlockProps() }>
{ __(
'Medium Mike's Basic Block – hello from the editor!',
'mediummike'
) }
</p>
)
}
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. For example, here’s how to modify edit.js
so it imports TextControl
and the message
attribute defined earlier:
import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import { TextControl } from '@wordpress/components';
import './editor.scss';
export default function Edit( { attributes, setAttributes } ) {
return (
<div { ...useBlockProps() }>
<TextControl
label={ __( 'Message', 'yourblockname' ) }
value={ attributes.message }
onChange={ ( val ) => setAttributes( { message: val } ) }
/>
</div>
);
}
The onChange
attribute is where the onChange()
function is assigned. This is important, as it’s how block attributes are updated.
The save.js
file is usually simpler than edit.js
, with fewer imports:
/**
* React hook that is used to mark the block wrapper element.
* It provides all the necessary props like the class name.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops
*/
import { useBlockProps } from '@wordpress/block-editor';
/**
* The save function defines the way in which the different attributes should
* be combined into the final markup, which is then serialized by the block
* editor into `post_content`.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#save
*
* @return {Element} Element to render.
*/
export default function save() {
return (
<p { ...useBlockProps.save() }>
{ 'Medium Mike's Basic Block – hello from the saved content!' }
</p>
);
}
To match the modified edit.js
file above that imports TextControl
and the message
attribute, the save.js
file would look like this:
import { useBlockProps } from '@wordpress/block-editor';
export default function save( { attributes } ) {
const blockProps = useBlockProps.save();
return <div { ...blockProps }>{ attributes.message }</div>;
}
The block now has some functionality. It doesn’t do much, but it meets the requirements of being a minimally functional block that can be inserted in the WordPress block editor.
A World of JavaScript
Blocks are made almost entirely in JavaScript. WordPress uses two actions, enqueue_block_assets
and enqueue_block_editor_assets
, to let you include your JavaScript and stylesheets to be used with the block editor. All you really need to do PHP-wise is enqueue your JavaScript and CSS files and call register_block_type()
with each asset’s handle.
Long story short, adding more functionality to your WordPress blocks requires sharp JavaScript skills, in particular the ESNext syntax and React. These skills are more common among WordPress developers than they were a few years ago, but there are still a lot of folks who are more comfortable (and faster!) doing as much as possible in PHP.
Creating Blocks With ACF
It’s a lot of work to create truly custom WordPress blocks, even with the scaffold commands and thorough documentation. ACF Blocks lets you create custom WordPress blocks, but in a PHP framework.
Both WordPress core blocks and ACF Blocks register their properties in block.json
, but ACF Blocks also have a PHP template for rendering the block, and a CSS file to apply any unique styling. Alternatively, you can skip the CSS file if you just want your ACF Block to inherit styles from your theme.
An ACF Block will often have an associated ACF field group, allowing it to pull data from the fields. However, it is perfectly feasible to create an ACF Block that does not use any ACF fields.
The configuration keys used in ACF Blocks are largely the same as the ones used in WordPress core blocks, including the use of camelCase. The primary difference is the addition of the acf configuration key, which controls the block’s display mode and template used for rendering.
Building an ACF Block
Just like creating native WordPress blocks, the first consideration is your development environment. You’ll need a copy of ACF PRO, as ACF Blocks is a premium feature that isn’t available in the free version of ACF, and a WordPress site where you have access to the wp-content
directory. We recommend spinning up a site in Local so you can experiment in complete safety.
With those in place, the process of creating an ACF Block can be broken down into six steps:
Create a child theme or plugin to hold your ACF Blocks
Create a
functions.php
fileRegister properties in
block.json
Associate the block with an existing field group, or create a new one (optional)
Create the template
Create the stylesheet (optional)
ACF’s Create Your First Block tutorial goes over these steps in detail, using a basic testimonial block as an example.
The tutorial shows every step of the process needed to create the testimonial block, from creating a child theme to applying styling. The block populates with data drawn from associated ACF fields, including a Text Area field to hold the quote, Text fields to enter the person’s name and role, an Image field, and Color Picker fields to set background and text colors.
Compared to creating native WordPress blocks, building ACF Blocks is much simpler, especially if you already know your way around WordPress and PHP templates. The process to populate the block is likely already familiar to your content editors: choose the ACF Block in the WordPress block inserter, fill in the ACF fields, make any desired adjustments, and it’s ready to publish:
Later tutorials from ACF build on the basic concepts, showing how to use InnerBlocks, block locking, and block context with your ACF Blocks.
Once you’ve got the basics down, check out how to build a slider block with ACF Blocks:
Wrapping Up
Custom blocks help expand your options when building WordPress sites, allowing content editors to expand their scope and creativity while staying within carefully designed parameters.
As long as the blocks you build serve their purpose, there’s no wrong way to create WordPress blocks. With that said, creating fully customized ACF Blocks has a much shorter learning curve, and allows you to easily draw in data from existing or new ACF field groups.
Have you created custom blocks with React, ACF Blocks, or both? Did you have a strong preference for one or the other? Let us know in the comments.