/    Sign up×
Articles /Pin to ProfileBookmark

How To Get Started Creating Your First Custom WordPress Block

There is a lot of great documentation out there for creating custom WordPress blocks, and I admit I’m a bit late to the party, but when I finally started learning how to do this it was a little difficult to find a single tutorial that gave me all the steps I needed. In this article I will attempt to break down the process in an easy to follow tutorial for those of you that are eager to dive into this brave new world. We will create a very simple block that allows the user to input some text and a link that displays like a button. You can create much more complex blocks, but since we’re just getting started, we’ll keep it simple to learn the basics.

Obviously, every single bit of knowledge and skill that is needed to get started can’t be covered in one article, so I am assuming you have at least basic experience with HTML, CSS, JavaScript, and React. If not, then it’s time to learn, after which you can make your way back here.

Let’s get started!

Set Up Your WordPress Development Environment

Again, there are a lot of tools to get you going with this. I suggest starting with Local, a tool that will get a local WordPress site up and running with pretty much a single click. You can download it here and find installation and set up instructions here.

Once WordPress is set up, you’ll need to install Node.js and npm (if not already installed). You can download Node here. Once you’ve installed Node.js, run the command node -v in your terminal to confirm installation. You can also run the npm -v command to confirm that you have the npm package available.

Create Your Block Plugin

This was a little confusing to me at first. Create a plugin for a block? But then I realized this makes it portable so you can use it on any of your WordPress sites.

Thankfully, there’s a dev-tool that will create all of the files and folders you need to create the plugin scaffolding and register a new block.

  1. From the command line, navigate to your /wp-content/plugins/ directory.
  2. Run this command: npx @wordpress/create-block name-of-your-block (obviously changing that last bit to your own custom block name).
  3. Enter y if/when asked to confirm

It will take a few minutes to finish, but then you can navigate to the Plugins screen in your WordPress admin dashboard and see your new plugin! Go ahead and activate it, then:

  1. Return to your terminal and navigate to your new plugin directory using cd name-of-your-block
  2. Run npm start (This will enable you to run the plugin in development mode and will compile all of your changes as you make them.)

Customizing Your Block

Now that we’re all set up, we’re going to start customizing the new block. All of your work will be done in /wp-content/plugins/name-of-your-block/src/, so navigate to that directory in your code editor of choice and:

  1. Open block.json.
  2. Edit the version, title, icon, category, and description as you see fit. These are what you will see in the WordPress edit screen when you’re adding a new block.
    NOTE: The icon property expects any Dashicon name as a string, see list of available icons. The category specified is a string and must be one of: “common, formatting, layout, widgets, or embed”. You can create your own custom category name, see documentation for details.
  3. In name-of-your-block.php, you can edit the Plugin Name, Description, Version, Author, etc. to match what you’ve done in block.json. This will change what you see on the Plugins screen in the admin dashboard.
  4. Open edit.js.
  5. Change the import statement on line 14 toimport { useBlockProps, RichText } from '@wordpress/block-editor';

    This will import the RichText component from the @wordpress/block-editor package, which allows developers to render a contenteditable input, providing users with the option to format block content to make it bold, italics, linked, or use other formatting.
  6. Now, on line 33, directly after export default function Edit() { include the corresponding useBlockProps element by addingconst blockProps = useBlockProps();It should now be as follows:
export default function Edit() {
  const blockProps = useBlockProps();
  return (
    <p { ...useBlockProps() }>
      { __( 'Test Block – hello from the editor!', 'test-block' ) }
    </p>
  );
}
  1. Next add the following code to replace what is currently being returned.Code to be replaced:
<p { ...useBlockProps() }>
  { __( 'Test Block – hello from the editor!', 'test-block' ) }
</p>{ __( 'Test Block – hello from the editor!', 'test-block' ) }

Replace with:

<div
  { ...blockProps }
>
  <RichText 
    tagName="span"
    onChange={ onChangeContent }
    allowedFormats={ [ 'core/bold', 'core/italic' ] }
    value={ __( content ) }
    placeholder={ __( 'Write your text...', 'name-of-your-block' ) }
    style={ { textAlign: align } }
  />
  <ExternalLink 
    href={ affiliateLink }
    className="btn btn-external-link"
    rel={ hasLinkNofollow ? "nofollow" : "" }
  >
    { linkLabel }
  </ExternalLink>
</div>

With this we’ve started creating our layout of the block, with a span that has a placeholder of “Write your text” and a button with the classes “btn btn-external-link”. It won’t actually work yet and will throw an error if you try to use it, because we still have more code to add.

  1. In block.json, add the following code in a new line 17 directly under "style": "file:./style-index.css"
    Be sure to add a comma after the above code on line 16, since we’ve added the additional attributes prop to our JSON.
"attributes": {
    "content": {
        "type": "string",
        "source": "html",
        "selector": "span"
    },
    "align": {
        "type": "string",
        "default": "none"
    },
    "affiliateLink": {
        "type": "string",
        "default": "https://"
    },
    "linkLabel": {
        "type": "string",
        "default": "Your Button Label"
    },
    "hasLinkNofollow": {
        "type": "boolean",
        "default": false
    }
}

Attributes provide information about the data stored by a block, such as rich content, background color, URLs, etc. For example, we added the span element as an HTML string in the first few lines. We’ve also added the text alignment, the URL field for the button, the label for the button, and the rel=nofollow attribute for the link.

  1. In edit.js, we need to pass the Edit function an object of properties, so change line 32 from:
export default function Edit() {

to:

export default function Edit( { attributes, setAttributes } ) {
const { content, align, affiliateLink, linkLabel, hasLinkNofollow } = attributes;
  1. Now we need to define the onChange attribute of the RichText element to provide a function to call when the element’s content changes. So add the following code in edit.js directly after const blockProps = useBlockProps(); (line 33):
const onChangeContent = ( newContent ) => {
setAttributes( { content: newContent } )
}
  1. We’ll do the same for our other elements just below that line.

const onChangeAlign = ( newAlign ) => {
    setAttributes( { 
        align: newAlign === undefined ? 'none' : newAlign, 
    } )
}

const onChangeAffiliateLink = ( newAffiliateLink ) => {
    setAttributes( { affiliateLink: newAffiliateLink === undefined ? '' : newAffiliateLink } )
}

const onChangeLinkLabel = ( newLinkLabel ) => {
    setAttributes( { linkLabel: newLinkLabel === undefined ? '' : newLinkLabel } )
}

const toggleNofollow = () => {
    setAttributes( { hasLinkNofollow: ! hasLinkNofollow } )
}

So now everything we just changed in your edit.js file should look like this:


export default function Edit( { attributes, setAttributes } ) {
    const blockProps = useBlockProps();
    
    const onChangeContent = ( newContent ) => {
	setAttributes( { content: newContent } )
    }
    
    const onChangeAlign = ( newAlign ) => {
	setAttributes( { 
	    align: newAlign === undefined ? 'none' : newAlign, 
	} )
    }
    
    const onChangeAffiliateLink = ( newAffiliateLink ) => {
        setAttributes( { affiliateLink: newAffiliateLink === undefined ? '' : newAffiliateLink } )
    }

    const onChangeLinkLabel = ( newLinkLabel ) => {
        setAttributes( { linkLabel: newLinkLabel === undefined ? '' : newLinkLabel } )
    }

    const toggleNofollow = () => {
        setAttributes( { hasLinkNofollow: ! hasLinkNofollow } )
    }
    
    return (
        <div
            { ...blockProps }
        >
            <RichText 
                tagName="span"
                onChange={ onChangeContent }
                allowedFormats={ [ 'core/bold', 'core/italic' ] }
                value={ __( content ) }
                placeholder={ __( 'Write your text...', 'name-of-your-block' ) }
                style={ { textAlign: align } }
            />
            <ExternalLink 
                href={ affiliateLink }
                className="btn btn-external-link"
                rel={ hasLinkNofollow ? "nofollow" : "" }
            >
                { linkLabel }
            </ExternalLink>
        </div>
    );
}
  1. Next, let’s add controls to our import in edit.js. Change line 14 from:
import { useBlockProps, RichText } from '@wordpress/block-editor';

to:

import {
  useBlockProps,
  RichText,
  AlignmentControl,
  BlockControls,
  InspectorControls
} from '@wordpress/block-editor';

This adds various components to the block toolbar.

  1. Just below that, at what is now line 14, add:
import {
  TextControl,
  PanelBody,
  PanelRow,
  ToggleControl,
  ExternalLink
} from '@wordpress/components';

This adds the controls we need for the Block Settings Sidebar.

  1. Now, to finish adding all of the controls, replace the entire return statement at the bottom of the edit.js file with the following:
return (
    <>
        <InspectorControls>
            <PanelBody 
                title={ __( 'Link Settings', 'name-of-your-block' )}
                initialOpen={true}
            >
                <PanelRow>
                    <fieldset>
                        <TextControl
                            label={__( 'External link', 'name-of-your-block' )}
                            value={ affiliateLink }
                            onChange={ onChangeAffiliateLink }
                            help={ __( 'Add your external link', 'name-of-your-block' )}
                        />
                    </fieldset>
                </PanelRow>
                <PanelRow>
                    <fieldset>
                        <TextControl
                            label={__( 'Link label', 'name-of-your-block' )}
                            value={ linkLabel }
                            onChange={ onChangeLinkLabel }
                            help={ __( 'Add link label', 'name-of-your-block' )}
                        />
                    </fieldset>
                </PanelRow>
                <PanelRow>
                    <fieldset>
                        <ToggleControl
                            label="Add rel = nofollow"
                            help={
                            hasLinkNofollow
                                ? 'Has rel nofollow.'
                                : 'No rel nofollow.'
                            }
                            checked={ hasLinkNofollow }
                            onChange={ toggleNofollow }
                        />
                    </fieldset>
                </PanelRow>
            </PanelBody>
        </InspectorControls>
        <BlockControls>
            <AlignmentControl
                value={ attributes.align }
                onChange={ onChangeAlign }
            />
        </BlockControls>
        <div
            { ...blockProps }
        >
            <RichText 
                tagName="span"
                onChange={ onChangeContent }
                allowedFormats={ [ 'core/bold', 'core/italic' ] }
                value={ __( content ) }
                placeholder={ __( 'Write your text...', 'name-of-your-block' ) }
                style={ { textAlign: align } }
            />
            <ExternalLink 
                href={ affiliateLink }
                className="btn btn-external-link"
                rel={ hasLinkNofollow ? "nofollow" : "" }
            >
                { linkLabel }
            </ExternalLink>
        </div>
    </>
);

With this code we’ve added all of our sidebar settings within Inspector Controls, and the alignment settings in the toolbar.

At this point, you should be able to create a new page and test the block. Since we haven’t styled it yet, it will look a bit funky, but it will have all of the elements we added so far. Now we have to modify the save function to store everything in the database.

  1. Open save.js
  2. On line 14, replace
import { useBlockProps } from '@wordpress/block-editor';

with

import { useBlockProps, RichText} from '@wordpress/block-editor';
  1. On line 25, replace
export default function save() {
    return (
        <p { ...useBlockProps.save() }>
            { __( 'Test Block – hello from the saved content!', 'test-block' ) }
        </p>
    );
}

with

export default function save( { attributes } ) {
    const { content, align, affiliateLink, linkLabel, hasLinkNofollow } = attributes; 
    const blockProps = useBlockProps.save();
    return (
        <div { ...blockProps }>
            <RichText.Content 
                tagName="span" 
                value={ content }
                style={ { textAlign: align } }
            />
            <a 
                href={ affiliateLink }
                className="btn btn-external-link"
                rel={ hasLinkNofollow ? "nofollow" : "noopener noreferrer" }
            >
                { linkLabel }
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" class="components-external-link__icon css-bqq7t3 etxm6pv0" role="img" aria-hidden="true" focusable="false"><path d="M18.2 17c0 .7-.6 1.2-1.2 1.2H7c-.7 0-1.2-.6-1.2-1.2V7c0-.7.6-1.2 1.2-1.2h3.2V4.2H7C5.5 4.2 4.2 5.5 4.2 7v10c0 1.5 1.2 2.8 2.8 2.8h10c1.5 0 2.8-1.2 2.8-2.8v-3.6h-1.5V17zM14.9 3v1.5h3.7l-6.4 6.4 1.1 1.1 6.4-6.4v3.7h1.5V3h-6.3z"></path></svg>
            </a>
        </div>
    );
}

Now the values entered into the block will be saved in the database and rendered on the front end.

We now have a fully functioning block! Only problem is that it’s not very pretty, so let’s fix that.

Styling Your Block

I’ll provide you with some sample CSS styling, but you can edit as you see fit. Basically, you should style the block to look the way you want it to look within the context of the front end, but know that it will also look this way on the back end edit screen for your users. The SASS is compiled all at once for both!

  1. Open style.scss
  2. Copy and/or edit the following code, replacing all that is currently in the file. Note that you will change the top level class name to match your block’s name.
.wp-block-create-block-name-of-your-block {
    align-items:center;
    border:none !important;
    display:flex;
    font-size:1em;
    .rich-text {
        width:40%;
    }
    .btn-external-link {
        align-items:center;
        background:#191919;
        border:none;
        border-radius:3px;
        color:#fff;
        display:flex;
        font-size:13px;
        margin-left:15px;
        padding:11px 15px;
        text-decoration: none;
        transition:background 0.15s linear;
        svg {
            margin-left:2px; 
            path {
                fill:#fff;
            }
        }
        &:hover {
            background:#31ACE3;
        }
    }
}

And that’s it! As you can see, there is a LOT that goes into creating the simplest of custom blocks, but hopefully this tutorial has eliminated any hesitancy you may have had with getting started and shown you that, although it’s a bit complex, there really isn’t much that’s difficult to understand. So go forth and create your own custom WordPress blocks!

CSSFront-endHTMLJavaScriptNode.jsPHPReactWordPress
×

Success!

Help @bkmacdaddy spread the word by sharing this article on Twitter...

Tweet This
Sign in
Forgot password?
Sign in with TwitchSign in with GithubCreate Account
about: ({
version: 0.1.9 BETA 4.25,
whats_new: community page,
up_next: more Davinci•003 tasks,
coming_soon: events calendar,
social: @webDeveloperHQ
});

legal: ({
terms: of use,
privacy: policy
});
changelog: (
version: 0.1.9,
notes: added community page

version: 0.1.8,
notes: added Davinci•003

version: 0.1.7,
notes: upvote answers to bounties

version: 0.1.6,
notes: article editor refresh
)...
recent_tips: (
tipper: @Yussuf4331,
tipped: article
amount: 1000 SATS,

tipper: @darkwebsites540,
tipped: article
amount: 10 SATS,

tipper: @Samric24,
tipped: article
amount: 1000 SATS,
)...