How to Create A Custom Block Without Plugin (Updated 2022)

Creating a custom Gutenberg block isn't an easy feat especially if you are not comfortable with JavaScript. We will you guide you slowly through the process

Creating a custom block isn’t an easy feat especially if you are not comfortable with pure JavaScript. Hopefully, this guide can help you with that.

Before Making Custom Block

First, ask this question before wasting time creating a redundant block:

“Is there an existing block that fits your need?”

For example, I once need to create this design in Gutenberg:

The general layout can be achieved with Media-Text block. But there’s no option to create a colored box behind the image. We also don’t have an option to add five stars below the Heading.

So we create 2 custom styles, one for Media-Text, and one for Heading:

register_block_style('core/media-text', [
  'name' => 'half-bg',
  'label' => 'Half Background'
]);

register_block_style('core/heading', [
  'name' => 'has-stars',
  'label' => 'Has Stars'
]);

Now we can use pseudoselector ::before and ::after to create that design like this:

.wp-block-media-text.is-style-half-bg {
  position: relative;
}
.wp-block-media-text.is-style-half-bg::before {
  content: "";
  position: absolute;
  top: 0;
  left: -3rem
  bottom: 0;
  width: 50%;
  background-color: #eee1cc;
}

h1.is-style-has-stars::after,
h2.is-style-has-stars::after,
h3.is-style-has-stars::after {
  content: "★★★★★";
  order: 2;
  display: block;
  width: 100%;
  font-size: 14px;
  letter-spacing: 2px;
  color: #ba9556;
}

If none of the existing blocks fits your need, continue the tutorial below.

1. Register Assets & Block Type

This tutorial uses this directory structure, adapt it according to your theme:

my-theme/
├─ css/
│  ├─ my-block.css
│  ├─ ...
├─ js/
│  ├─ my-block.js
│  ├─ ...
├─ functions.php
├─ ...
  • my-block.js is for the admin editor.
  • my-block.css is for both the front end and the admin editor.

Now let’s enqueue them:

add_action('wp_enqueue_scripts', 'my_enqueue_assets');
add_action('enqueue_block_editor_assets', 'my_enqueue_block_assets');

function my_enqueue_assets() {
  $css_dir = get_stylesheet_directory_uri() . '/css';
  wp_enqueue_style('my-block', $css_dir . '/my-block.css', []);
}

function my_enqueue_block_assets() {
  $css_dir = get_stylesheet_directory_uri() . '/css';
  $js_dir = get_stylesheet_directory_uri() . '/js';

  // If in plugin, use this instead:
  // $css_dir = plugin_dir_url(__FILE__) . 'css';
  // $js_dir = plugin_dir_url(__FILE__) . 'js';

  wp_enqueue_script('my-block', $js_dir . '/my-block.js', [ 'wp-blocks', 'wp-dom' ] , null, true);
  wp_enqueue_style('my-block', $css_dir . '/my-block.css', [ 'wp-edit-blocks' ]);
}

2. What We Are Building

We will build a simple text field. It’s pretty much useless since we have Paragraph block, but this is just to introduce you to the syntax:

Our “Simple Text” custom block in action

3. The Script

Gutenberg uses a library called React. If you are not familiar with it, the code below will look taunting. But don’t worry, we will explain it the best we could.

(() => {
  const el = window.wp.element.createElement;
  const { registerBlockType } = window.wp.blocks;
  const { RichText } = window.wp.blockEditor;

  registerBlockType('my/simple-text', {
    title: 'Simple Text (Custom)',
    icon: 'universal-access-alt',
    category: 'layout',
    attributes: {
      content: {
        type: 'array',
        source: 'children',
        selector: 'p',
      },
    },
    edit: myEdit,
    save: mySave
  });
  
  // what's rendered in admin
  function myEdit(props) {
    const atts = props.attributes;

    return el(RichText, {
      tagName: 'p',
      className: props.className,
      value: atts.content,

      // Listener when the RichText is changed.
      onChange: (value) => {
        props.setAttributes({ content: value });
      },
    });
  }
  
  // what's saved in database and rendered in frontend
  function mySave(props) {
    const atts = props.attributes;

    return el(RichText.Content, {
      tagName: 'p',
      value: atts.content,
    });
  }
})();

Let’s break down the code:

SELF INVOKING FUNCTION

(() => {
  ...
})();

This wrapper is used to enclose our code. So any variables or functions defined here won’t leak to the outside.

WP BLOCKS LIBRARY

const el = window.wp.element.createElement;
const { registerBlockType } = window.wp.blocks;
const { RichText } = window.wp.blockEditor;

Gutenberg already provided us with many reusable functions and elements such wp.blockEditor.RichText.

The code above is just re-assigning the variable so it’s easier to use later.

REGISTER BLOCK

registerBlockType('my/simple-text', {
  title: 'Simple Text (Custom)',
  icon: 'universal-access-alt',
  category: 'layout',
  attributes: {
    content: {
      type: 'array',
      source: 'children',
      selector: 'p',
    },
  },
  edit: myEdit,
  save: mySave
});
  • my/simple-text – The block’s slug. This will translate into the wrapper’s class name like “wp-block-my-simple-text”.
  • title – Is what appears when selecting the block.
  • iconClick here for a list of available icons.
  • attributes – List of variables for the block. source and selector defines how to extract the value from saved content. More info»
  • edit – Callback on how to render the content in the editor.
  • save – Callback on how to render the content before saved to the database.

EDIT

function myEdit(props) {
  const atts = props.attributes;

  return el(RichText, {
    tagName: 'p',
    className: props.className,
    value: atts.content,

    // Listener when the RichText is changed.
    onChange: (value) => {
      props.setAttributes({ content: value });
    },
  });
}

Edit function returns the elements that are rendered in Gutenberg. Here’s the breakdown:

  • el(...) – create an HTML element.
  • RichText – a special element from Gutenberg. It’s like <input type="text"> but has a toolbar to bold, italic, anchor link, etc.
  • tagName – wraps the RichText with <p>.
  • value – uses the content variable we defined in attributes as the value of this RichText.
  • onChange – updates the content variable as we type.

SAVE

function mySave(props) {
  const atts = props.attributes;

  return el(RichText.Content, {
    tagName: 'p',
    value: atts.content,
  });
}

The HTML returned here is saved into the database. In turn, this is also what is shown on the front end.

4. The Style

Our custom block is just a standard field text, so the CSS is just this:

.wp-block-my-simple-text {
  border: 1px solid rgba(0,0,0,.1);
  padding: 1rem;
}

Done! Go check out your block in the editor. You should see your custom block listed like this:

Selecting the custom block

Conclusion

Creating a custom block is difficult and can get ugly. So try to use a custom block style as shown above whenever possible.

If you are interested to learn more, we made an extensive tutorial on our Github. It also included the code with React JSX syntax:

Feel free to leave a question below regarding custom block 🙂

Note: This article is originally written in March 2019. It has been updated to reflect the standard way in the latest WP version.

Default image
Henner Setyono
A web developer who mainly build custom theme for WordPress since 2013. Young enough to never had to deal with using Table as layout.
Leave a Reply to StevanCancel Reply

11 Comments

  1. Shouldn't the path to the file in point 2 and 3 be '/assets/js/my-block-admin.js' - same as enqued scripts file?

  2. Mobile Image for Cover Block (Gutenberg) | WP Tips

    […] The JS is long and complex, so copy it from our gist. If you want to learn the syntax, check out our other tutorial. […]

  3. I literally copied and pasted and it's not working on my website.

    Is there something else I need to do?

    • Hi, your theme folder structure probably different from mine

      • Hi Henner,

        I copied the way you posted it on the website. It's the same structure, I'm using the 2020 theme.

        • Hi, I received your email containing more detail about the error. It might be because you're in a child theme and therefore need to use get_template_directory_uri() instead of get_stylesheet_directory_uri() when enqueuing it.

          If you still have trouble, we can continue the conversation in email. Thanks

  4. This doesn't work. All it does is show an icon. I have a feeling lots of steps are missing. Are there suppose to be .css files? If so, what for? This kind of coding is new to a lot of people, if you know more, it would be nice to share or at least point people to the right recourses.

    • Hi Cathy, I think your JS and CSS aren't enqueued. Make sure the directory and file name point to your CSS and JS file.

      I will make an edit to explain some stuff further. Thanks for the input.

      You can also refer to this Github repo containing all the Gutenberg tutorials I wrote: https://github.com/hrsetyono/gutenberg-tutorial

      It's a working plugin, so you can install it and check out the blocks.

  5. I have this error : "props is not defined" ?