All skills
Skillintermediate
Gutenberg Blocks
WordPress 6.4+ uses the Block Editor (Gutenberg) as the primary editing experience. Blocks are the fundamental building units.
Claude Code Knowledge Pack7/10/2026
Overview
Gutenberg Blocks
Block Development Overview
WordPress 6.4+ uses the Block Editor (Gutenberg) as the primary editing experience. Blocks are the fundamental building units.
Block Types
| Type | Description | Use Case |
|---|---|---|
| Static | Fixed HTML output | Simple content, images |
| Dynamic | Server-rendered | Posts list, dynamic data |
| Interactive | Client-side JS | Accordions, tabs, carousels |
Project Setup
Using @wordpress/create-block
# Create a new block plugin
npx @wordpress/create-block my-block --namespace my-plugin
# Create with specific template
npx @wordpress/create-block my-block --template @wordpress/create-block-interactive-template
# Create dynamic block
npx @wordpress/create-block my-block --variant dynamic
Generated Structure
my-block/
├── my-block.php # Plugin file
├── package.json # NPM dependencies
├── src/
│ ├── block.json # Block metadata
│ ├── edit.js # Editor component
│ ├── save.js # Frontend save
│ ├── index.js # Block registration
│ ├── editor.scss # Editor styles
│ └── style.scss # Frontend styles
├── build/ # Compiled assets
└── readme.txt
package.json Scripts
{
"name": "my-block",
"version": "1.0.0",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start",
"format": "wp-scripts format",
"lint:js": "wp-scripts lint-js",
"lint:css": "wp-scripts lint-style",
"packages-update": "wp-scripts packages-update"
},
"devDependencies": {
"@wordpress/scripts": "^27.0.0"
}
}
Block Registration
block.json (WordPress 6.4+)
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my-plugin/my-block",
"version": "1.0.0",
"title": "My Block",
"category": "widgets",
"icon": "smiley",
"description": "A custom block for displaying content.",
"keywords": ["custom", "content", "block"],
"supports": {
"html": false,
"align": ["wide", "full"],
"anchor": true,
"color": {
"background": true,
"text": true,
"link": true,
"gradients": true
},
"spacing": {
"margin": true,
"padding": true,
"blockGap": true
},
"typography": {
"fontSize": true,
"lineHeight": true
},
"__experimentalBorder": {
"color": true,
"radius": true,
"style": true,
"width": true
}
},
"attributes": {
"content": {
"type": "string",
"source": "html",
"selector": "p"
},
"alignment": {
"type": "string",
"default": "left"
},
"showBorder": {
"type": "boolean",
"default": false
},
"items": {
"type": "array",
"default": []
},
"selectedPostId": {
"type": "number"
}
},
"example": {
"attributes": {
"content": "Example content for the block preview."
}
},
"textdomain": "my-plugin",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"viewScript": "file:./view.js",
"render": "file:./render.php"
}
PHP Registration
<?php
declare(strict_types=1);
/**
* Register all blocks
*/
function my_plugin_register_blocks(): void {
// Auto-register from block.json
register_block_type(__DIR__ . '/build/my-block');
// Or with additional arguments
register_block_type(__DIR__ . '/build/another-block', [
'render_callback' => 'my_plugin_render_another_block',
]);
}
add_action('init', 'my_plugin_register_blocks');
/**
* Register block category
*/
function my_plugin_block_categories(array $categories): array {
return array_merge(
[
[
'slug' => 'my-plugin-blocks',
'title' => __('My Plugin Blocks', 'my-plugin'),
'icon' => 'wordpress',
],
],
$categories
);
}
add_filter('block_categories_all', 'my_plugin_block_categories');
Static Block Development
index.js (Entry Point)
/**
* WordPress dependencies
*/
/**
* Internal dependencies
*/
/**
* Register block
*/
registerBlockType(metadata.name, {
edit: Edit,
save,
});
edit.js (Editor Component)
/**
* WordPress dependencies
*/
useBlockProps,
RichText,
InspectorControls,
BlockControls,
AlignmentToolbar,
MediaUpload,
MediaUploadCheck,
} from '@wordpress/block-editor';
PanelBody,
ToggleControl,
TextControl,
SelectControl,
RangeControl,
Button,
} from '@wordpress/components';
/**
* Edit component
*
* @param {Object} props Block props
* @param {Object} props.attributes Block attributes
* @param {Function} props.setAttributes Function to update attributes
* @return {JSX.Element} Block edit component
*/
const { content, alignment, showBorder, imageId, imageUrl, columns } = attributes;
const blockProps = useBlockProps({
className: `align-${alignment}${showBorder ? ' has-border' : ''}`,
});
const onSelectImage = (media) => {
setAttributes({
imageId: media.id,
imageUrl: media.url,
});
};
const onRemoveImage = () => {
setAttributes({
imageId: undefined,
imageUrl: undefined,
});
};
return (
<>
setAttributes({ alignment: newAlignment })
}
/>
setAttributes({ showBorder: value })}
/>
setAttributes({ columns: value })}
min={1}
max={6}
/>
setAttributes({ layout: value })}
/>
(
<div className="editor-post-featured-image">
{imageUrl ? (
<>
<img src={imageUrl} alt="" />
{__('Remove Image', 'my-plugin')}
</>
) : (
{__('Select Image', 'my-plugin')}
)}
</div>
)}
/>
<div {...blockProps}>
{imageUrl && (
<img src={imageUrl} alt="" className="block-image" />
)}
setAttributes({ content: value })}
placeholder={__('Enter content...', 'my-plugin')}
allowedFormats={['core/bold', 'core/italic', 'core/link']}
/>
</div>
</>
);
}
save.js (Frontend Output)
/**
* WordPress dependencies
*/
/**
* Save component
*
* @param {Object} props Block props
* @param {Object} props.attributes Block attributes
* @return {JSX.Element} Block save component
*/
const { content, alignment, showBorder, imageUrl } = attributes;
const blockProps = useBlockProps.save({
className: `align-${alignment}${showBorder ? ' has-border' : ''}`,
});
return (
<div {...blockProps}>
{imageUrl && (
<img src={imageUrl} alt="" className="block-image" />
)}
<RichText.Content tagName="p" value={content} />
</div>
);
}
Dynamic Block Development
Dynamic blocks render on the server, useful for content that changes or requires PHP logic.
block.json for Dynamic Block
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my-plugin/recent-posts",
"title": "Recent Posts",
"category": "widgets",
"icon": "list-view",
"description": "Display recent posts with customizable options.",
"supports": {
"html": false,
"align": ["wide", "full"]
},
"attributes": {
"numberOfPosts": {
"type": "number",
"default": 5
},
"postType": {
"type": "string",
"default": "post"
},
"showExcerpt": {
"type": "boolean",
"default": true
},
"showFeaturedImage": {
"type": "boolean",
"default": true
},
"categories": {
"type": "array",
"default": []
}
},
"textdomain": "my-plugin",
"editorScript": "file:./index.js",
"style": "file:./style-index.css",
"render": "file:./render.php"
}
edit.js for Dynamic Block
/**
* WordPress dependencies
*/
PanelBody,
RangeControl,
ToggleControl,
SelectControl,
Spinner,
} from '@wordpress/components';
/**
* Edit component for dynamic block
*/
const { numberOfPosts, postType, showExcerpt, showFeaturedImage } = attributes;
const blockProps = useBlockProps();
// Fetch post types for select
const postTypes = useSelect((select) => {
const { getPostTypes } = select(coreStore);
const types = getPostTypes({ per_page: -1 });
return types?.filter((type) => type.viewable && type.rest_base) || [];
}, []);
// Fetch categories
const categories = useSelect((select) => {
const { getEntityRecords } = select(coreStore);
return getEntityRecords('taxonomy', 'category', { per_page: -1 }) || [];
}, []);
const postTypeOptions = postTypes.map((type) => ({
label: type.labels.singular_name,
value: type.slug,
}));
return (
<>
setAttributes({ numberOfPosts: value })}
min={1}
max={20}
/>
{postTypeOptions.length > 0 && (
setAttributes({ postType: value })}
/>
)}
setAttributes({ showExcerpt: value })}
/>
setAttributes({ showFeaturedImage: value })}
/>
<div {...blockProps}>
(
<div className="loading-placeholder">
<p>{__('Loading...', 'my-plugin')}</p>
</div>
)}
EmptyResponsePlaceholder={() => (