Creating Your First Custom Gutenberg Block (The Modern JavaScript Way)
Building custom Gutenberg blocks opens up endless possibilities for extending WordPress. Whether you're creating unique content layouts, integrating third-party services, or building interactive features, custom blocks give you complete control over the editing experience.
This comprehensive guide walks you through creating your first custom block using modern JavaScript and WordPress's official build tools. We'll start with a simple block and progressively add features, ensuring you understand not just the "how" but also the "why" behind each step.
Table of Contents
- Development Environment Setup
- Block Scaffolding with @wordpress/create-block
- Block Attributes and Controls
- Save and Edit Functions
- Block Styles and Variations
- Publishing to the Directory
Development Environment Setup
Prerequisites
Before creating your first block, ensure you have:
- Node.js and npm: Version 14.0 or higher
- Local WordPress Environment: Using Local, DevKinsta, or wp-env
- Code Editor: VS Code recommended with ESLint extension
- Basic Knowledge: JavaScript ES6+, React basics, WordPress fundamentals
Verifying Your Setup
# Check Node.js version
node --version
# Should output v14.0.0 or higher
# Check npm version
npm --version
# Should output 6.0.0 or higher
# Check WordPress version (in WordPress admin)
# Should be 5.8 or higher for best block support
Installing Development Tools
# Install WordPress development tools globally
npm install -g @wordpress/create-block
# Verify installation
npx @wordpress/create-block --version
Setting Up Your Development Workspace
# Navigate to your WordPress plugins directory
cd /path/to/wordpress/wp-content/plugins/
# Create a new directory for block development
mkdir my-custom-blocks
cd my-custom-blocks
Block Scaffolding with @wordpress/create-block
Creating Your First Block
The @wordpress/create-block
tool provides the fastest way to bootstrap a new block with modern build configuration.
# Create a new block plugin
npx @wordpress/create-block my-first-block
# Navigate to the block directory
cd my-first-block
# Start development mode
npm start
Understanding the Generated Structure
my-first-block/
├── build/ # Compiled production files
├── node_modules/ # Dependencies
├── src/
│ ├── block.json # Block metadata
│ ├── index.js # Block registration
│ ├── edit.js # Editor component
│ ├── save.js # Frontend output
│ ├── editor.scss # Editor styles
│ └── style.scss # Frontend + Editor styles
├── .editorconfig
├── .gitignore
├── package.json
├── readme.txt
└── my-first-block.php # Plugin file
Exploring Key Files
block.json - Block Metadata
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "create-block/my-first-block",
"version": "0.1.0",
"title": "My First Block",
"category": "widgets",
"icon": "smiley",
"description": "Example block scaffolded with Create Block tool.",
"example": {},
"supports": {
"html": false
},
"textdomain": "my-first-block",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"viewScript": "file:./view.js"
}
index.js - Block Registration
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
import save from './save';
import metadata from './block.json';
registerBlockType( metadata.name, {
edit: Edit,
save,
} );
edit.js - Editor Component
import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import './editor.scss';
export default function Edit() {
return (
<p { ...useBlockProps() }>
{ __( 'My First Block – hello from the editor!', 'my-first-block' ) }
</p>
);
}
Block Attributes and Controls
Adding Block Attributes
Attributes store the block's data. Let's create a testimonial block with various controls:
Update block.json
{
"attributes": {
"content": {
"type": "string",
"source": "html",
"selector": "p.testimonial-content"
},
"authorName": {
"type": "string",
"source": "html",
"selector": "p.testimonial-author"
},
"authorTitle": {
"type": "string",
"default": ""
},
"showImage": {
"type": "boolean",
"default": true
},
"imageUrl": {
"type": "string",
"default": ""
},
"imageId": {
"type": "number",
"default": 0
},
"backgroundColor": {
"type": "string",
"default": "#f0f0f0"
},
"textColor": {
"type": "string",
"default": "#333333"
},
"borderRadius": {
"type": "number",
"default": 8
}
}
}
Implementing Block Controls
Enhanced edit.js with Controls
import { __ } from '@wordpress/i18n';
import {
useBlockProps,
RichText,
InspectorControls,
BlockControls,
MediaUpload,
MediaUploadCheck,
PanelColorSettings,
AlignmentToolbar,
} from '@wordpress/block-editor';
import {
PanelBody,
TextControl,
ToggleControl,
RangeControl,
Button,
Toolbar,
ToolbarButton,
} from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import './editor.scss';
export default function Edit( { attributes, setAttributes } ) {
const {
content,
authorName,
authorTitle,
showImage,
imageUrl,
imageId,
backgroundColor,
textColor,
borderRadius,
textAlignment,
} = attributes;
const blockProps = useBlockProps( {
style: {
backgroundColor,
color: textColor,
borderRadius: `${borderRadius}px`,
padding: '2rem',
},
} );
// Get image data from Media Library
const image = useSelect(
( select ) => {
return imageId ? select( 'core' ).getMedia( imageId ) : null;
},
[ imageId ]
);
const onSelectImage = ( media ) => {
setAttributes( {
imageUrl: media.url,
imageId: media.id,
} );
};
const onRemoveImage = () => {
setAttributes( {
imageUrl: '',
imageId: 0,
} );
};
return (
<>
<BlockControls>
<AlignmentToolbar
value={ textAlignment }
onChange={ ( alignment ) =>
setAttributes( { textAlignment: alignment } )
}
/>
<Toolbar>
<ToolbarButton
icon="admin-appearance"
label={ __( 'Style Options', 'my-first-block' ) }
onClick={ () => console.log( 'Style options clicked' ) }
/>
</Toolbar>
</BlockControls>
<InspectorControls>
<PanelBody
title={ __( 'Testimonial Settings', 'my-first-block' ) }
initialOpen={ true }
>
<ToggleControl
label={ __( 'Show Author Image', 'my-first-block' ) }
checked={ showImage }
onChange={ ( value ) => setAttributes( { showImage: value } ) }
/>
{ showImage && (
<MediaUploadCheck>
<MediaUpload
onSelect={ onSelectImage }
allowedTypes={ [ 'image' ] }
value={ imageId }
render={ ( { open } ) => (
<div className="image-selector">
{ imageUrl ? (
<div className="image-preview">
<img src={ imageUrl } alt="" />
<Button
isSecondary
isSmall
onClick={ onRemoveImage }
>
{ __( 'Remove', 'my-first-block' ) }
</Button>
</div>
) : (
<Button isPrimary onClick={ open }>
{ __( 'Select Image', 'my-first-block' ) }
</Button>
) }
</div>
) }
/>
</MediaUploadCheck>
) }
<TextControl
label={ __( 'Author Title/Company', 'my-first-block' ) }
value={ authorTitle }
onChange={ ( value ) => setAttributes( { authorTitle: value } ) }
/>
<RangeControl
label={ __( 'Border Radius', 'my-first-block' ) }
value={ borderRadius }
onChange={ ( value ) => setAttributes( { borderRadius: value } ) }
min={ 0 }
max={ 50 }
/>
</PanelBody>
<PanelColorSettings
title={ __( 'Color Settings', 'my-first-block' ) }
colorSettings={ [
{
value: backgroundColor,
onChange: ( color ) =>
setAttributes( { backgroundColor: color } ),
label: __( 'Background Color', 'my-first-block' ),
},
{
value: textColor,
onChange: ( color ) => setAttributes( { textColor: color } ),
label: __( 'Text Color', 'my-first-block' ),
},
] }
/>
</InspectorControls>
<div { ...blockProps }>
<blockquote
className="testimonial-wrapper"
style={ { textAlign: textAlignment } }
>
<RichText
tagName="p"
className="testimonial-content"
value={ content }
onChange={ ( value ) => setAttributes( { content: value } ) }
placeholder={ __( 'Write testimonial…', 'my-first-block' ) }
/>
<div className="testimonial-author-wrapper">
{ showImage && imageUrl && (
<img
src={ imageUrl }
alt=""
className="testimonial-author-image"
/>
) }
<div className="testimonial-author-info">
<RichText
tagName="p"
className="testimonial-author"
value={ authorName }
onChange={ ( value ) =>
setAttributes( { authorName: value } )
}
placeholder={ __( 'Author name…', 'my-first-block' ) }
/>
{ authorTitle && (
<p className="testimonial-author-title">{ authorTitle }</p>
) }
</div>
</div>
</blockquote>
</div>
</>
);
}
Save and Edit Functions
Understanding the Save Function
The save function determines how your block appears on the frontend:
save.js - Frontend Output
import { useBlockProps, RichText } from '@wordpress/block-editor';
export default function save( { attributes } ) {
const {
content,
authorName,
authorTitle,
showImage,
imageUrl,
backgroundColor,
textColor,
borderRadius,
textAlignment,
} = attributes;
const blockProps = useBlockProps.save( {
style: {
backgroundColor,
color: textColor,
borderRadius: `${borderRadius}px`,
padding: '2rem',
},
} );
return (
<div { ...blockProps }>
<blockquote
className="testimonial-wrapper"
style={ { textAlign: textAlignment } }
>
<RichText.Content
tagName="p"
className="testimonial-content"
value={ content }
/>
<div className="testimonial-author-wrapper">
{ showImage && imageUrl && (
<img
src={ imageUrl }
alt=""
className="testimonial-author-image"
/>
) }
<div className="testimonial-author-info">
<RichText.Content
tagName="p"
className="testimonial-author"
value={ authorName }
/>
{ authorTitle && (
<p className="testimonial-author-title">{ authorTitle }</p>
) }
</div>
</div>
</blockquote>
</div>
);
}
Block Deprecation Strategy
When updating block structure, implement deprecations to maintain backward compatibility:
// In index.js
import deprecated from './deprecated';
registerBlockType( metadata.name, {
edit: Edit,
save,
deprecated,
} );
// deprecated.js
const v1 = {
attributes: {
// Previous attributes
},
save( { attributes } ) {
// Previous save function
},
};
export default [ v1 ];
Block Styles and Variations
Adding Block Styles
style.scss - Frontend & Editor Styles
.wp-block-create-block-my-first-block {
.testimonial-wrapper {
margin: 0;
position: relative;
&::before {
content: '"';
font-size: 4rem;
position: absolute;
top: -1rem;
left: -1rem;
opacity: 0.2;
line-height: 1;
}
}
.testimonial-content {
font-size: 1.125rem;
line-height: 1.6;
margin-bottom: 1.5rem;
font-style: italic;
}
.testimonial-author-wrapper {
display: flex;
align-items: center;
gap: 1rem;
}
.testimonial-author-image {
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: cover;
}
.testimonial-author-info {
.testimonial-author {
margin: 0;
font-weight: 600;
font-style: normal;
}
.testimonial-author-title {
margin: 0;
opacity: 0.7;
font-size: 0.875rem;
}
}
}
editor.scss - Editor-Only Styles
.wp-block-create-block-my-first-block {
.image-selector {
margin-top: 1rem;
.image-preview {
position: relative;
img {
max-width: 100%;
height: auto;
display: block;
margin-bottom: 0.5rem;
}
}
}
// Add visual hints in editor
.testimonial-content:empty::before {
content: attr(placeholder);
opacity: 0.5;
}
}
Creating Block Variations
Add variations to block.json:
{
"variations": [
{
"name": "testimonial-centered",
"title": "Centered Testimonial",
"attributes": {
"textAlignment": "center"
},
"isDefault": true
},
{
"name": "testimonial-left",
"title": "Left Aligned Testimonial",
"attributes": {
"textAlignment": "left"
}
}
],
"styles": [
{
"name": "default",
"label": "Default",
"isDefault": true
},
{
"name": "plain",
"label": "Plain"
},
{
"name": "bordered",
"label": "Bordered"
}
]
}
Block Supports
Enable additional features through block supports:
{
"supports": {
"html": false,
"align": ["wide", "full"],
"anchor": true,
"customClassName": true,
"spacing": {
"margin": true,
"padding": true
},
"color": {
"text": true,
"background": true,
"gradients": true
},
"typography": {
"fontSize": true,
"lineHeight": true
}
}
}
Publishing to the Directory
Preparing for Release
1. Production Build
# Create optimized production build
npm run build
# Check build output
ls -la build/
2. Plugin Header Requirements
Update my-first-block.php
:
<?php
/**
* Plugin Name: My First Block
* Description: Example block scaffolded with Create Block tool.
* Requires at least: 6.1
* Requires PHP: 7.0
* Version: 0.1.0
* Author: Your Name
* Author URI: https://example.com
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: my-first-block
*
* @package create-block
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
function create_block_my_first_block_block_init() {
register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'create_block_my_first_block_block_init' );
3. readme.txt Requirements
=== My First Block ===
Contributors: yourusername
Tags: block, gutenberg, testimonial
Tested up to: 6.3
Stable tag: 0.1.0
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
A beautiful testimonial block for the WordPress block editor.
== Description ==
This plugin adds a customizable testimonial block to your WordPress block editor. Features include:
* Rich text editing for testimonial content
* Author name and title fields
* Optional author image
* Customizable colors and border radius
* Multiple style variations
== Installation ==
1. Upload the plugin files to the `/wp-content/plugins/my-first-block` directory
2. Activate the plugin through the 'Plugins' screen in WordPress
3. Search for "My First Block" in the block editor
== Frequently Asked Questions ==
= How do I customize the testimonial appearance? =
Use the block settings in the sidebar to adjust colors, border radius, and other visual properties.
== Screenshots ==
1. The testimonial block in the editor
2. Block settings panel
3. Frontend display
== Changelog ==
= 0.1.0 =
* Initial release
Submission Process
1. Create Plugin ZIP
# From plugin directory
npm run plugin-zip
# This creates my-first-block.zip
2. Test the ZIP
- Install on a fresh WordPress site
- Test all functionality
- Check for console errors
- Verify frontend display
3. Submit to WordPress.org
- Create a WordPress.org account
- Go to https://wordpress.org/plugins/developers/add/
- Upload your ZIP file
- Fill in plugin details
- Submit for review
Post-Submission
Setting Up SVN
# After approval, clone SVN repository
svn co https://plugins.svn.wordpress.org/my-first-block
# Copy files to trunk
cp -r /path/to/plugin/* trunk/
# Add files
svn add trunk/* --force
# Commit
svn ci -m "Initial release"
# Tag release
svn cp trunk tags/0.1.0
svn ci -m "Tagging version 0.1.0"
Advanced Block Development
Adding Dynamic Data
// Fetch data from WordPress
import { useSelect } from '@wordpress/data';
const posts = useSelect( ( select ) => {
return select( 'core' ).getEntityRecords( 'postType', 'post', {
per_page: 3,
} );
}, [] );
Creating Inner Blocks
import { InnerBlocks } from '@wordpress/block-editor';
const ALLOWED_BLOCKS = [ 'core/paragraph', 'core/image' ];
const TEMPLATE = [
[ 'core/paragraph', { placeholder: 'Add content...' } ],
];
// In edit function
<InnerBlocks
allowedBlocks={ ALLOWED_BLOCKS }
template={ TEMPLATE }
/>
// In save function
<InnerBlocks.Content />
Adding Block Transforms
transforms: {
from: [
{
type: 'block',
blocks: [ 'core/paragraph' ],
transform: ( { content } ) => {
return createBlock( 'create-block/my-first-block', {
content,
} );
},
},
],
to: [
{
type: 'block',
blocks: [ 'core/paragraph' ],
transform: ( { content } ) => {
return createBlock( 'core/paragraph', {
content,
} );
},
},
],
}
Best Practices
1. Performance Optimization
- Lazy load heavy components
- Minimize re-renders with useMemo
- Optimize asset loading
2. Accessibility
- Use semantic HTML
- Add ARIA labels
- Ensure keyboard navigation
3. Internationalization
import { __ } from '@wordpress/i18n';
// Always wrap strings
__( 'Text to translate', 'text-domain' )
4. Error Handling
try {
// Block logic
} catch ( error ) {
console.error( 'Block error:', error );
return (
<div { ...blockProps }>
{ __( 'Block error. Please check settings.', 'my-first-block' ) }
</div>
);
}
Conclusion
Creating custom Gutenberg blocks opens up incredible possibilities for extending WordPress. With modern JavaScript tools and WordPress's robust block API, you can build powerful, user-friendly blocks that enhance the editing experience.
Key takeaways: - Use @wordpress/create-block for quick setup - Understand the relationship between edit and save functions - Leverage WordPress components for consistent UI - Test thoroughly before publishing - Follow WordPress coding standards
Start simple, iterate often, and don't be afraid to explore the vast ecosystem of WordPress blocks. Your first block is just the beginning of your journey into modern WordPress development.
Ready to dive deeper into WordPress development? Explore our Ultimate Guide to Gutenberg and Full-Site Editing for more advanced techniques and best practices.