Two Ways to Create Custom WordPress Blocks

By Mike Davey, Senior Editor

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.

  1. What Are WordPress Blocks?
  2. Creating Blocks With React
    1. Setting Up Your Dev Environment
    2. Block Plugin File Structure
    3. Block Metadata in block.json
    4. Setting Block Attributes
    5. Edit and Save
    6. A World of JavaScript
  3. Creating Blocks With ACF
    1. Building an ACF Block
  4. Wrapping Up

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:

  • A code editor, such as VS Code
  • Node.js dev tools
  • A local WordPress site

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
? 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):
? 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.

The "welcome-view-site" icon and a logo from the Paranoia rpg, side by side.

Tell me I’m wrong.

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
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(, {
     * @see ./edit.js
    edit: Edit,

     * @see ./save.js
} );

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 the style 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 the edit.js file. The edit.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 the save.js file. The save.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 the block.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": "",
    "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 supports align, it means that the block can be aligned to the left, right, or center. If the block supports anchor, 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 the src 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 the src 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 the src 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 the src 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
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
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
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
 * @return {Element} Element to render.
export default function Edit() {
    return (
        <p { ...useBlockProps() }>
            { __(
                'Medium Mike&#39;s Basic Block – hello from the editor!',
            ) }

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() }>
                label={ __( 'Message', 'yourblockname' ) }
                value={ attributes.message }
                onChange={ ( val ) => setAttributes( { message: val } ) }

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
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
 * @return {Element} Element to render.
export default function save() {
    return (
        <p { }>
            { 'Medium Mike&#39;s Basic Block – hello from the saved content!' }

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 =;
    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 view of the WordPress Block Inserter, showing the "Widgets" section, with the newly created block at the end of the section.

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 file

  • Register 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.

A basic testimonial block created with ACF.

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:

YouTube cover image

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.

About the Author

Mike Davey Senior Editor

Mike is an editor and writer based in Hamilton, Ontario, with an extensive background in business-to-business communications and marketing. His hobbies include reading, writing, and wrangling his four children.