How to create a custom Gutenberg block from scratch with @wordpress/scripts?

So, you want to build a custom Gutenberg block from the ground up using @wordpress/scripts? Great! This is the most efficient and recommended way to do it. Essentially, @wordpress/scripts is a package that bundles all the necessary build tools (like Webpack, Babel, ESLint, PostCSS, and more) that WordPress uses internally. This means you don’t have to spend hours configuring everything yourself; you can jump straight into writing your block’s code.

Let’s dive in.

Instead of manually setting up Webpack, Babel, and all the other tools required to compile modern JavaScript and React for your Gutenberg block, @wordpress/scripts does it for you. Think of it as a pre-configured toolkit that’s optimized for WordPress development. It handles things like compiling JSX, transpiling ESNext to ES5 for wider browser support, minifying your code, and even setting up a local development server with live reloading.

The Problem with Manual Setup

If you’ve ever tried to configure Webpack from scratch, you know it can be a steep learning curve. You need to understand loaders, plugins, entry points, output paths, and more. Then add Babel presets for React and modern JavaScript, ESLint for code quality, and PostCSS for CSS preprocessing. It’s a lot of boilerplate that takes time away from actually building your block.

The @wordpress/scripts Solution

@wordpress/scripts abstracts all of that complexity away. You get a set of pre-configured scripts that you can run from your package.json file. This means you can focus on the what and not the how when it comes to your build process. It also ensures your blocks are built using the same standards and best practices that WordPress itself employs.

If you’re interested in learning more about creating custom Gutenberg blocks, you might find this article on building a comprehensive WordPress plugin particularly useful. It delves into the intricacies of plugin development while integrating custom blocks, providing a broader context for your Gutenberg block creation journey. Check it out here: Building a Comprehensive WordPress Plugin.

Setting Up Your Development Environment

Before we write any code, we need to get our environment ready. This involves a few basic steps that will ensure you have a clean slate to work from.

Initializing Your Plugin

First, create a new directory for your plugin within your WordPress installation’s wp-content/plugins folder. Let’s call it my-custom-block.

“`bash

cd /path/to/your/wordpress/wp-content/plugins

mkdir my-custom-block

cd my-custom-block

“`

Next, initialize a package.json file. This file will manage your project’s dependencies and scripts.

“`bash

npm init -y

“`

The -y flag answers “yes” to all the prompts, giving you a default package.json. You can edit this later if you wish.

Installing Dependencies

Now, install @wordpress/scripts as a development dependency.

“`bash

npm install –save-dev @wordpress/scripts

“`

You’ll also need React and ReactDOM, as Gutenberg blocks are built using React.

“`bash

npm install –save-dev react react-dom

“`

Structuring Your Project

A common and sensible project structure for a Gutenberg block plugin looks like this:

“`

my-custom-block/

├── my-custom-block.php

├── package.json

├── src/

│ ├── block.json

│ ├── index.js

│ ├── editor.scss

│ └── style.scss

└── build/ (this directory will be created automatically)

├── block.json

├── index.js

├── index.asset.php

├── editor.css

└── style.css

“`

  • my-custom-block.php: This is your main plugin file, where you’ll register your block.
  • package.json: Manages dependencies and scripts.
  • src/: Contains your source code (JavaScript, CSS, block configuration).
  • block.json: Defines your block’s metadata (name, title, attributes, etc.).
  • index.js: The main JavaScript file for your block, containing its edit and save functions.
  • editor.scss: Stylesheets applied only in the Gutenberg editor.
  • style.scss: Stylesheets applied to both the editor and the frontend.
  • build/: This directory will hold the compiled and optimized assets produced by @wordpress/scripts. You should not manually edit files in this directory.

Let’s create the src directory and the files within it for now:

“`bash

mkdir src

touch src/block.json src/index.js src/editor.scss src/style.scss

“`

Crafting Your First Block

Now that the environment is set up, let’s start building the actual block. We’ll create a simple block that displays a customizable message.

Defining Your Block with block.json

The block.json file is a modern way to register and define your block. It’s often referred to as the “block registration JSON.” It contains all the metadata for your block and helps WordPress automatically queue up assets.

Inside src/block.json, add the following:

“`json

{

“apiVersion”: 2,

“name”: “my-custom-block/basic-message”,

“title”: “Basic Message Block”,

“category”: “common”,

“icon”: “text-page”,

“description”: “A simple block to display a custom message.”,

“textdomain”: “my-custom-block”,

“attributes”: {

“message”: {

“type”: “string”,

“source”: “html”,

“selector”: “p”

}

},

“supports”: {

“html”: false,

“anchor”: true

},

“editorScript”: “file:./index.js”,

“editorStyle”: “file:./editor.css”,

“style”: “file:./style.css”

}

“`

Let’s break down some key parts:

  • apiVersion: Always set this to 2 for modern blocks.
  • name: A unique identifier for your block, formatted as namespace/block-name.
  • title: The user-friendly title displayed in the editor.
  • category: The category your block will appear under (e.g., common, text, media).
  • icon: A Dashicon slug or SVG HTML for your block’s icon.
  • description: A short description for your block.
  • textdomain: Used for internationalization.
  • attributes: Defines the data your block stores. Here, message is a string attribute that will be saved from a

    tag.

  • supports: Features supported by your block (e.g., html for raw HTML editing, anchor for custom IDs).
  • editorScript, editorStyle, style: These tell WordPress where to find your compiled JavaScript (index.js), editor-specific CSS (editor.css), and frontend/editor CSS (style.css). The file: prefix indicates relative paths from the block.json file. @wordpress/scripts handles the compilation from .js to .js and .scss to .css.

Writing Your Block’s JavaScript (index.js)

This is where the magic happens. Your index.js file will define how your block behaves in the editor (edit function) and how it’s saved to the database (save function).

Inside src/index.js, add:

“`jsx

import { registerBlockType } from ‘@wordpress/blocks’;

import { useBlockProps, RichText } from ‘@wordpress/block-editor’;

import metadata from ‘./block.json’;

import ‘./editor.scss’;

import ‘./style.scss’;

registerBlockType( metadata.name, {

/**

  • @see ./block.json

*/

title: metadata.title,

description: metadata.description,

category: metadata.category,

icon: metadata.icon,

attributes: metadata.attributes,

textdomain: metadata.textdomain,

supports: metadata.supports,

/**

  • @arg {Object} props Props for the block.
  • @arg {Object} props.attributes The block attributes.
  • @arg {Function} props.setAttributes Function to update block attributes.
  • @return {JSX.Element} Element to render in the editor.

*/

edit: ( { attributes, setAttributes } ) => {

const blockProps = useBlockProps();

const { message } = attributes;

return (

tagName=”p”

value={ message }

onChange={ ( newMessage ) => setAttributes( { message: newMessage } ) }

placeholder=”Enter your message here…”

/>

);

},

/**

  • @arg {Object} props Props for the block.
  • @arg {Object} props.attributes The block attributes.
  • @return {JSX.Element} Element to render on the frontend.

*/

save: ( { attributes } ) => {

const blockProps = useBlockProps.save();

const { message } = attributes;

return (

{ message }

);

},

} );

“`

A quick rundown of this code:

  • Imports: We’re importing necessary functions and components from the @wordpress/blocks and @wordpress/block-editor packages. metadata is imported directly from block.json.
  • registerBlockType: This is the main function for registering your block. It takes the block name (from metadata.name) and an object containing its configuration.
  • edit function: This defines what your block looks like and how it behaves within the Gutenberg editor.
  • useBlockProps(): A hook that provides default CSS classes and other attributes to your block’s wrapper element. Essential for proper block styling and functionality.
  • RichText: A core Gutenberg component that allows rich text editing (bold, italic, links, etc.). We’re using it to edit our message attribute. Its tagName prop defines the HTML element it will ultimately render.
  • onChange: Updates the message attribute whenever the content in RichText changes.
  • save function: This defines the static HTML that will be saved to the database and rendered on the frontend.
  • useBlockProps.save(): Similar to useBlockProps() but tailored for the save function.
  • We’re rendering a simple

    tag with the message attribute.

Adding Stylesheets (editor.scss and style.scss)

These files will contain your block’s styling using SCSS. @wordpress/scripts will compile these into standard CSS.

src/editor.scss:

“`scss

.wp-block-my-custom-block-basic-message {

border: 1px dashed #dedede;

padding: 15px;

background-color: #f7f7f7;

text-align: center;

.components-placeholder { // Style our placeholder inside the editor

color: #999;

}

}

“`

This style will only apply when the block is in the editor. Notice the specific CSS class wp-block-my-custom-block-basic-message. This class is automatically generated by WordPress based on your block’s name (my-custom-block/basic-message). You can also use blockProps.className if you generate a custom class, but the default is often sufficient.

src/style.scss:

“`scss

.wp-block-my-custom-block-basic-message {

padding: 20px;

background-color: #e0f2f7;

margin-bottom: 20px;

font-family: sans-serif;

color: #333;

border-radius: 5px;

box-shadow: 0 2px 5px rgba(0,0,0,0.1);

text-align: center;

}

“`

This style will apply to both the editor and the frontend.

Integrating with WordPress (The PHP File)

Now that we have our block’s definition and logic, we need to tell WordPress about it. This happens in our main plugin file, my-custom-block.php.

“`php

/**

  • Plugin Name: My Custom Basic Block
  • Description: A simple custom Gutenberg block.
  • Version: 1.0.0
  • Author: Your Name
  • Text Domain: my-custom-block
  • Domain Path: /languages

*

  • @package MyCustomBlock

*/

if ( ! defined( ‘ABSPATH’ ) ) {

exit; // Exit if accessed directly.

}

/**

  • Registers the block using the metadata loaded from block.json.
  • It’s important to do this in the init hook, so WordPress has
  • already initialized all dependencies.

*/

function my_custom_block_basic_block_init() {

register_block_type( __DIR__ . ‘/build’ );

}

add_action( ‘init’, ‘my_custom_block_basic_block_init’ );

“`

Here’s what’s happening:

  • Plugin Header: Standard WordPress plugin header comments.
  • Security Check: if ( ! defined( 'ABSPATH' ) ) { exit; } is a best practice to prevent direct access to your plugin files.
  • my_custom_block_basic_block_init(): This function is hooked into init.
  • register_block_type( __DIR__ . '/build' ): This is the crucial part. By pointing register_block_type to the build directory, WordPress automatically reads the block.json file located there. The block.json then tells WordPress where to find the compiled index.js, editor.css, and style.css files. This is the beauty of block.json‘s asset loading capabilities.

If you’re interested in enhancing your WordPress development skills, you might find it helpful to explore a related article on creating custom Gutenberg blocks. This resource provides a comprehensive guide that complements the tutorial on how to create a custom Gutenberg block from scratch with @wordpress/scripts. By diving into this article, you can gain a deeper understanding of the nuances involved in block development. For more information, check out this helpful guide that can further assist you in your learning journey.

Compiling Your Block with @wordpress/scripts

We’ve written all the source code; now we need to compile it. This is where @wordpress/scripts comes in.

Adding Build Scripts to package.json

Open your package.json file and add the following scripts section:

“`json

{

“name”: “my-custom-block”,

“version”: “1.0.0”,

“description”: “”,

“main”: “index.js”,

“scripts”: {

“build”: “wp-scripts build”,

“start”: “wp-scripts start”

},

“keywords”: [],

“author”: “”,

“license”: “ISC”,

“devDependencies”: {

“@wordpress/scripts”: “^26.0.0”,

“react”: “^18.2.0”,

“react-dom”: “^18.2.0”

}

}

“`

  • build: This script runs wp-scripts build. It compiles your development-ready code into optimized, production-ready assets (minified, transpiled, etc.).
  • start: This script runs wp-scripts start. It compiles your code in development mode and watches for changes, automatically recompiling and refreshing your browser (if you have your development server configured correctly). This is super useful for live development.

Running the Build Process

In your plugin’s root directory (my-custom-block), run the build command:

“`bash

npm run build

“`

You should see output indicating that the compilation was successful. If you check your project structure, you’ll now see a new build directory containing your compiled assets, including index.js, index.asset.php, editor.css, and style.css. The index.asset.php file is also automatically generated and contains dependency information (like React) for WordPress’s script enqueuing system.

Starting the Development Server

For an even smoother development workflow, use the start script:

“`bash

npm run start

“`

This will run in watch mode. Now, as you make changes to your src files, @wordpress/scripts will automatically recompile them. You’ll typically need to refresh your browser tab (or use a browser extension that detects file changes if you’re not running a full browser sync tool) to see the updates in the editor.

If you’re interested in enhancing your WordPress development skills, you might find it beneficial to explore how to create a custom Gutenberg block from scratch with @wordpress/scripts. This process allows you to tailor your website’s functionality and design to better suit your needs. Additionally, you may want to check out this informative article on migrating to another server, which provides valuable insights that can complement your understanding of WordPress management and deployment.

Testing Your Custom Block

With the build process complete and your PHP file set up, it’s time to see your block in action.

Activating the Plugin

  1. Log in to your WordPress admin dashboard.
  2. Navigate to Plugins > Installed Plugins.
  3. Find “My Custom Basic Block” and click Activate.

Adding the Block to a Post or Page

  1. Create a new post or page, or edit an existing one.
  2. Click the + icon to add a new block.
  3. Search for “Basic Message Block” (or find it under the “Common” category).
  4. Click on your block to add it.

You should now see your block in the editor. Type a message into the RichText component. Observe the editor styles you defined in editor.scss.

Previewing the Block

  1. Update or publish your post/page.
  2. View the post/page on the frontend.

You should see your message styled according to style.scss. The editor-specific styles should not be present.

Advanced Features and Best Practices

As you build more complex blocks, you’ll encounter situations where you need more fine-grained control or want to incorporate additional features. @wordpress/scripts provides excellent support for many of these.

Customizing Webpack Configuration

While @wordpress/scripts works out-of-the-box, sometimes you need to tweak the underlying Webpack configuration. You can do this by creating a webpack.config.js file in your plugin’s root directory. wp-scripts will automatically merge your custom configuration with its default one.

For example, to add an alias or a custom Webpack plugin:

“`javascript

// webpack.config.js

const defaultConfig = require( ‘@wordpress/scripts/config/webpack.config’ );

const path = require( ‘path’ );

module.exports = {

…defaultConfig,

resolve: {

…defaultConfig.resolve,

alias: {

// Example: Alias a common component directory

‘@components’: path.resolve( __dirname, ‘src/components’ ),

},

},

// You can also add custom plugins, loaders, etc.

};

“`

Important: When customizing, always start by importing and spreading the defaultConfig to ensure you don’t override essential settings.

Internationalization (i18n)

Making your block translatable is crucial for wider adoption. @wordpress/scripts handles this automatically for strings wrapped with WordPress’s i18n functions.

Modify your index.js like so:

“`jsx

import { registerBlockType } from ‘@wordpress/blocks’;

import { useBlockProps, RichText } from ‘@wordpress/block-editor’;

import { __ } from ‘@wordpress/i18n’; // Import the translation function

import metadata from ‘./block.json’;

import ‘./editor.scss’;

import ‘./style.scss’;

registerBlockType( metadata.name, {

// … (other properties)

edit: ( { attributes, setAttributes } ) => {

const blockProps = useBlockProps();

const { message } = attributes;

return (

tagName=”p”

value={ message }

onChange={ ( newMessage ) => setAttributes( { message: newMessage } ) }

placeholder={ __( ‘Enter your message here…’, ‘my-custom-block’ ) } // Wrap strings for translation

/>

);

},

save: ( { attributes } ) => {

const blockProps = useBlockProps.save();

const { message } = attributes;

return (

{ message }

);

},

} );

“`

When you run npm run build, wp-scripts will automatically extract these translatable strings into a POT file, which can then be used to create translation files (PO/MO).

Adding Inspector Controls

For more complex blocks, you’ll want to add controls to the block sidebar (Inspector Controls) for options like text alignment, color settings, or toggle switches.

This involves importing components from @wordpress/components and using the component in your edit function.

Example snippet for index.js (inside edit function):

“`jsx

import { InspectorControls } from ‘@wordpress/block-editor’;

import { PanelBody, TextControl } from ‘@wordpress/components’;

// … other imports

edit: ( { attributes, setAttributes } ) => {

// … existing code

return (

label={ __( ‘Custom ID’, ‘my-custom-block’ ) }

value={ attributes.customId } // Assuming you added a ‘customId’ attribute

onChange={ ( newId ) => setAttributes( { customId: newId } ) }

/>

{/ … your block’s main content /}

);

},

“`

Remember to add customId to your attributes object in block.json first!

Using the __experimentalBlock API

WordPress periodically introduces experimental APIs for blocks. While not always recommended for production without careful consideration (as they might change), they offer insights into future capabilities. You can use these in your blocks if you’re comfortable with potential breaking changes in future WordPress versions. Generally, for most custom blocks, you’ll stick to the stable APIs.

Linting Your Code

@wordpress/scripts includes ESLint integration by default. It uses the official WordPress ESLint configuration. To check your code for stylistic issues and potential errors:

“`bash

npm run lint-js

“`

Or, for stylesheets:

“`bash

npm run lint-style

“`

These are useful commands to integrate into your development workflow or CI/CD pipeline to maintain code quality.

Troubleshooting Common Issues

Even with a streamlined toolkit, you might run into bumps. Here are a few common ones and how to approach them.

build Directory Not Updating

  • Did you run npm run build or npm run start? If you only edited files and didn’t trigger a build, your build folder won’t update.
  • Are there compilation errors? Check your terminal output after running the script. Webpack or Babel errors will prevent successful compilation. Look for red text.
  • Clear browser cache: Sometimes the browser caches older versions of your compiled assets. A hard refresh (Ctrl+Shift+R or Cmd+Shift+R) or disabling cache in dev tools can help.

Block Not Showing in Editor

  • Is your plugin activated? Double-check in the WordPress admin.
  • Errors in the browser console? Open your browser’s developer tools (usually F12) and check the “Console” tab. JavaScript errors in your block’s code can prevent it from being rendered.
  • Correct block name in registerBlockType()? Ensure the name property in block.json matches the first argument passed to registerBlockType in index.js (which is typically metadata.name).
  • Correct path in register_block_type() in PHP? Make sure __DIR__ . '/build' points to the correct location of your block.json file.
  • block.json syntax valid? Even a small typo (e.g., missing comma, unclosed bracket) can invalidate the JSON. Use an online JSON validator if in doubt.

Styles Not Applying

  • Correct CSS selector? Ensure you’re using the correct block class (.wp-block-your-name-your-block) or any custom class you’ve added via useBlockProps.
  • CSS Specificity? Other WordPress or theme styles might be overriding yours. Use browser developer tools to inspect the element and see which styles are being applied. You might need to make your selectors more specific or use !important as a last resort (generally, avoid !important).
  • Are editor.scss and style.scss correctly imported/enqueued? Check your block.json‘s editorStyle and style fields. Then, ensure your index.js file imports them (import './editor.scss'; import './style.scss';).
  • Browser cache: Again, a common culprit. Clear it.

Building custom Gutenberg blocks can feel a bit complex at first due to the modern JavaScript tooling, but @wordpress/scripts significantly simplifies the process. By following these steps, you’ve not only created a fundamental custom block but also established a robust development workflow that will serve you well for more intricate projects. Keep experimenting and building!