Claude skill gutenberg block
Best practices for creating custom WordPress Gutenberg blocks with React/TypeScript, covering scaffolding, block architecture, attributes, dynamic vs static rendering, and testingFrom its SKILL.md
npx -y skills add DaveRobinson/claude-skill--gutenberg-blockAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its file declares
Copied from the file, not written here
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
26.0 KB, ~6.3k tokens by cl100k_base, as published. Nobody here has run it
WordPress Gutenberg Custom Block Development
This skill provides guidance for creating custom Gutenberg blocks for WordPress using modern JavaScript/TypeScript and React.
Prerequisites & Environment Setup
Required Knowledge:
- WordPress plugin development fundamentals
- JavaScript ES6+ / TypeScript
- React basics (components, hooks, JSX)
- Understanding of WordPress block editor concepts
Development Environment:
- Node.js and npm installed
- WordPress development environment (Local, wp-env, or Docker)
- Code editor with TypeScript/React support
Essential Packages:
@wordpress/create-block- Official scaffolding tool@wordpress/scripts- Build tooling (webpack, babel, etc.)@wordpress/components- UI component librarywp-env(optional) - Local WordPress environment via Docker
Quick Start: Scaffolding a Block
Using @wordpress/create-block
Basic scaffolding (static block):
cd /path/to/wordpress/wp-content/plugins
npx @wordpress/create-block@latest my-custom-block --namespace=my-namespace
cd my-custom-block
npm start
Dynamic block with standard template:
npx @wordpress/create-block@latest my-dynamic-block --namespace=my-namespace --variant dynamic
Interactive template (always dynamic, uses Interactivity API):
# JavaScript variant (default)
npx @wordpress/create-block@latest my-interactive-block --template @wordpress/create-block-interactive-template
# TypeScript variant
npx @wordpress/create-block@latest my-interactive-block --template @wordpress/create-block-interactive-template --variant typescript
Note: Interactive template blocks are always dynamic (use render.php) and include the WordPress Interactivity API for reactive frontend experiences.
Key flags:
--namespace- Your unique namespace (required to avoid conflicts)--no-plugin- Scaffold block files only (no plugin wrapper)--wp-env- Add wp-env configuration for local development--category- Set block category (text, media, design, widgets, theme, embed)
Generated Structure
my-custom-block/
├── build/ # Compiled production code (don't edit)
├── node_modules/ # Dependencies (don't commit)
├── src/ # Your development files
│ ├── block.json # Block metadata (THE BRAIN)
│ ├── edit.js # Editor component
│ ├── save.js # Frontend output (static blocks)
│ ├── style.scss # Frontend styles
│ └── editor.scss # Editor-only styles
├── package.json
└── my-custom-block.php # Plugin entry point
Development Workflow
After scaffolding a block:
1. Get the plugin into your WordPress environment:
- Copy/move to your WordPress plugins folder, or
- Use
wp-envfor a containerized WordPress environment (see below)
2. Start development or build for production:
# Development (watches for changes, rebuilds automatically)
npm start
# Production (optimized, minified build)
npm run build
Using wp-env:
# 1. Start build process
npm start # or npm run build for production
# 2. Launch WordPress environment
npx wp-env start
# Access at http://localhost:8888
# Admin: http://localhost:8888/wp-admin (admin/password)
Note: For development, run npm start in one terminal, then wp-env start in another (npm start runs continuously).
Skill Scope: Single Block Per Plugin
This skill focuses on the standard scaffolding pattern: one block per plugin. The structure is:
my-custom-block/
├── src/
│ ├── block.json # Block metadata at src root
│ ├── edit.js
│ ├── save.js
│ ├── style.scss
│ └── editor.scss
├── build/ # Compiled output
└── my-custom-block.php # Plugin entry
Multiple blocks per plugin requires advanced build configuration and is outside this skill's scope. For theme-based blocks or multiple blocks, consult the Block Development Examples repository.
Post-Scaffolding: Customize Default Styles
The scaffolded style.scss contains placeholder styles meant to be customized:
// src/style.scss - SCAFFOLDED PLACEHOLDER
.wp-block-my-namespace-my-block {
background-color: #21759b; // Placeholder - customize or remove
color: #fff; // Placeholder - customize or remove
padding: 2px;
}
Why customize:
style.scssapplies to BOTH editor AND frontend- Placeholder styles may not match your design
- For reusable blocks, keep minimal to respect user themes
Recommended approach:
// src/style.scss - CUSTOMIZED
/**
* Shared styles (editor and frontend).
* Keep minimal for maximum theme compatibility.
*/
.wp-block-my-namespace-my-block {
// Add only essential structural styles
// Let themes control colors, typography, spacing
}
Note: editor.scss is editor-only and can have more specific styling for the editing experience.
Core Concepts
1. block.json - The Block's Brain
The block.json file is the single source of truth for block metadata:
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my-namespace/my-block",
"title": "My Custom Block",
"category": "widgets",
"icon": "smiley",
"description": "A custom block example",
"keywords": ["custom", "example"],
"version": "1.0.0",
"textdomain": "my-custom-block",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"attributes": {
"content": {
"type": "string",
"source": "html",
"selector": "p"
}
},
"supports": {
"html": false,
"anchor": true,
"align": true
}
}
Critical fields:
apiVersion- Use 3 for latest featuresname- Must be unique:namespace/block-nameicon- Visual identifier (see Icon Selection below)attributes- Define data structure and storagesupports- Enable/disable core features
Icon Selection
Icons appear in the block inserter and help users identify your block quickly.
Built-in Dashicons (easiest):
"icon": "smiley"
Common choices:
- Text/content:
"text","editor-paragraph","editor-alignleft" - Media:
"format-image","format-video","format-gallery" - Layout:
"layout","columns","grid-view" - Interactive:
"button","forms","testimonial" - Data:
"chart-bar","analytics","database" - Social:
"share","email","twitter"
Full list: https://developer.wordpress.org/resource/dashicons/
Custom SVG icon:
"icon": {
"src": "<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><path d='M12 2L2 7v10c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V7l-10-5z' fill='currentColor'/></svg>"
}
SVG with custom colors:
"icon": {
"background": "#7e70af",
"foreground": "#fff",
"src": "<svg>...</svg>"
}
Best practices:
- Keep SVG viewBox at
0 0 24 24for consistency - Use
currentColorfor fill/stroke to respect theme colors - Avoid complex SVGs (keep paths simple)
- Test icon visibility in both light and dark editor themes
- Choose icons that visually represent the block's purpose
- For brand blocks, use brand colors in background/foreground
2. Attributes - Data Management
Attributes control how blocks store and retrieve data:
"attributes": {
"title": {
"type": "string",
"source": "html",
"selector": "h2",
"default": "Default Title"
},
"isActive": {
"type": "boolean",
"default": false
},
"items": {
"type": "array",
"default": []
},
"settings": {
"type": "object",
"default": {}
}
}
Type options: string, boolean, number, integer, array, object, null
Source options:
attribute- Extract from HTML attributetext- Extract text contenthtml- Extract inner HTMLquery- Extract multiple elementsmeta- Store in post meta
3. Static vs Dynamic Blocks
Scaffolding Choice:
- Static block:
npx @wordpress/create-block my-block - Dynamic block:
npx @wordpress/create-block my-block --variant dynamic
Using --variant dynamic sets up the correct structure from the start.
Static Blocks:
- HTML saved to database at save time
- Content persists even if plugin deactivated
- Requires manual updates (re-save post)
- Best for: Content that rarely changes, distributed plugins
// edit.js
export default function Edit({ attributes, setAttributes }) {
return (
<div {...useBlockProps()}>
<RichText
tagName="p"
value={attributes.content}
onChange={(content) => setAttributes({ content })}
/>
</div>
);
}
// save.js
export default function Save({ attributes }) {
return (
<div {...useBlockProps.save()}>
<RichText.Content tagName="p" value={attributes.content} />
</div>
);
}
Dynamic Blocks:
- Rendered via PHP at runtime
- Always current (updates automatically)
- More database queries
- Best for: Theme-specific blocks, data that changes, client projects
Scaffolded with --variant dynamic:
// render.php (automatically created)
<?php
/**
* Available variables:
* @var array $attributes The block attributes
* @var string $content The block default content
* @var WP_Block $block The block instance
*/
?>
<div <?php echo get_block_wrapper_attributes(); ?>>
<?php echo esc_html( $attributes['content'] ?? 'Default content' ); ?>
</div>
// index.js - No save function needed
registerBlockType( metadata.name, {
edit: Edit, // Only edit function
} );
block.json configuration:
{
"render": "file:./render.php",
"viewScript": "file:./view.js" // Optional frontend JS
}
Converting static to dynamic: If you scaffolded a static block and need to convert it:
- Create
render.phpin the block directory - Add
"render": "file:./render.php"toblock.json - Remove the save function from
index.js - Rebuild with
npm run build
Decision Guide:
- Theme-specific design/layout → Dynamic (theme-coupled)
- Functionality/features → Static (plugin)
- Real-time data (posts, users, API) → Dynamic
- User-generated content → Static
- Open source distribution → Static
4. Block Controls
Toolbar Controls (quick access):
import { BlockControls } from '@wordpress/block-editor';
import { ToolbarGroup, ToolbarButton } from '@wordpress/components';
<BlockControls>
<ToolbarGroup>
<ToolbarButton
icon="admin-links"
label="Add Link"
onClick={() => {/* handle */}}
/>
</ToolbarGroup>
</BlockControls>
Inspector Panel (detailed settings):
import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody, ToggleControl, TextControl } from '@wordpress/components';
<InspectorControls>
<PanelBody title="Settings" initialOpen={true}>
<ToggleControl
label="Enable Feature"
checked={attributes.isActive}
onChange={(isActive) => setAttributes({ isActive })}
/>
<TextControl
label="Custom Text"
value={attributes.customText}
onChange={(customText) => setAttributes({ customText })}
/>
</PanelBody>
</InspectorControls>
TypeScript Integration
Setting Up TypeScript (Manual)
1. Install dependencies:
npm install --save-dev typescript @types/react @types/wordpress__block-editor @types/wordpress__blocks @types/wordpress__components
2. Create tsconfig.json:
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"lib": ["dom", "esnext"],
"jsx": "react",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
3. Update webpack.config.js:
const defaultConfig = require('@wordpress/scripts/config/webpack.config');
module.exports = {
...defaultConfig,
resolve: {
...defaultConfig.resolve,
extensions: ['.tsx', '.ts', '.js', '.jsx']
},
module: {
...defaultConfig.module,
rules: [
...defaultConfig.module.rules,
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
}
};
4. Type your Edit component:
// edit.tsx
import { useBlockProps } from '@wordpress/block-editor';
interface EditProps {
attributes: {
content: string;
isActive: boolean;
};
setAttributes: (attrs: Partial<EditProps['attributes']>) => void;
className?: string;
}
export default function Edit({ attributes, setAttributes, className }: EditProps) {
const blockProps = useBlockProps();
return (
<div {...blockProps}>
{/* Your block UI */}
</div>
);
}
Common Patterns
Pattern: Custom Post Query Block
// Dynamic block that queries posts
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
export default function Edit({ attributes }) {
const { postsPerPage = 5 } = attributes;
const posts = useSelect((select) => {
return select(coreStore).getEntityRecords('postType', 'post', {
per_page: postsPerPage,
_embed: true
});
}, [postsPerPage]);
if (!posts) return <Spinner />;
return (
<div {...useBlockProps()}>
{posts.map(post => (
<article key={post.id}>
<h3>{post.title.rendered}</h3>
</article>
))}
</div>
);
}
Pattern: Block with InnerBlocks (Static)
import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
// edit.js
export default function Edit() {
const TEMPLATE = [
['core/heading', { level: 2, placeholder: 'Section Title' }],
['core/paragraph', { placeholder: 'Section content...' }]
];
return (
<div {...useBlockProps()}>
<InnerBlocks template={TEMPLATE} />
</div>
);
}
// save.js
export default function Save() {
return (
<div {...useBlockProps.save()}>
<InnerBlocks.Content />
</div>
);
}
Pattern: Dynamic Block with InnerBlocks
⚠️ Important Official Guidance: Per the Block Editor Handbook:
"For many dynamic blocks, the
savecallback function should be returned asnull... If you are using InnerBlocks in a dynamic block you will need to save the InnerBlocks in the save callback function using<InnerBlocks.Content/>"
This pattern is essential when you need to process or transform inner blocks on the server.
// edit.js
import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
export default function Edit() {
const TEMPLATE = [
['core/group', {
className: 'slot-one',
metadata: { name: 'slot-one-content' }
}, [
['core/paragraph', { placeholder: 'First slot content...' }]
]],
['core/group', {
className: 'slot-two',
metadata: { name: 'slot-two-content' }
}, [
['core/paragraph', { placeholder: 'Second slot content...' }]
]]
];
return (
<div {...useBlockProps()}>
<InnerBlocks template={TEMPLATE} />
</div>
);
}
// save.js
// Must save InnerBlocks even though block is dynamic!
export default function save() {
return (
<div {...useBlockProps.save()}>
<InnerBlocks.Content />
</div>
);
}
PHP Render Callback:
The $block parameter provides access to inner blocks as WP_Block objects. Use the render() method to output them (Code Reference).
function my_block_render_callback( $attributes, $content, $block ) {
// Inner blocks are WP_Block objects
$inner_blocks = $block->inner_blocks;
$slot_one = '';
$slot_two = '';
// Render using ->render() method for WP_Block objects
if ( isset( $inner_blocks[0] ) ) {
$slot_one = $inner_blocks[0]->render();
}
if ( isset( $inner_blocks[1] ) ) {
$slot_two = $inner_blocks[1]->render();
}
// Transform inner content for custom output
return sprintf(
'<custom-element><div slot="one">%s</div><div slot="two">%s</div></custom-element>',
$slot_one,
$slot_two
);
}
register_block_type( __DIR__ . '/build', [
'render_callback' => 'my_block_render_callback'
] );
Key Methods (WordPress 6.0+):
WP_Block::render()- ForWP_Blockobjects from$block->inner_blocksrender_block($array)- For parsed block arrays fromparse_blocks()
Common Use Cases:
- Wrapping inner blocks with custom HTML (web components, special layouts)
- Adding server-side data or context to nested content
- Conditionally showing/hiding sections based on attributes
- Processing inner blocks based on user permissions or state
Pattern: Block Variations
// block.json
"variations": [
{
"name": "blue-variant",
"title": "Blue Style",
"icon": "admin-appearance",
"attributes": {
"backgroundColor": "blue"
}
},
{
"name": "red-variant",
"title": "Red Style",
"icon": "admin-appearance",
"attributes": {
"backgroundColor": "red"
}
}
]
Pattern: Frontend JavaScript with Blocks
Blocks can load frontend JavaScript using viewScript in block.json:
{
"viewScript": "file:./view.js"
}
Or reference multiple script handles (your own, core, or third-party):
{
"viewScript": ["file:./view.js", "wp-api-fetch", "my-external-library"]
}
External scripts are registered using standard WordPress wp_register_script() in your plugin's main PHP file.
Important: Define all scripts in block.json's viewScript array. Don't use view_script_handles in register_block_type() as it overrides the viewScript from block.json.
Note: Scripts only enqueue on pages where the block is used.
Testing & Quality Assurance
Manual Testing Checklist
When developing or modifying a block, verify:
Registration & Discovery:
- Block appears in inserter with correct icon/title
- Block appears in correct category
- Block can be searched by keywords
Functionality:
- Block can be added to editor without errors
- All toolbar controls work as expected
- Inspector panel controls update attributes correctly
- Attributes save and restore correctly
- Block renders correctly on frontend
- Dynamic blocks fetch and display current data
Compatibility:
- Works with block themes and classic themes
- Responsive across mobile/tablet/desktop
- No JavaScript console errors or warnings
- Works in posts, pages, and custom post types
- Works in widget areas (if applicable)
- Compatible with Full Site Editing (if applicable)
Accessibility:
- Keyboard navigation works throughout
- Screen reader announces elements correctly
- ARIA labels present where needed
- Focus indicators visible
- Color contrast meets WCAG standards
Performance:
- No unnecessary re-renders
- Scripts/styles only load when block present
- Images optimized and lazy-loaded
- No blocking JavaScript
Validation Testing
Block Deprecation: When changing save output, always test migration:
deprecated: [
{
attributes: { /* old structure */ },
save: OldSaveFunction,
migrate: (attributes) => {
// Transform old to new
return newAttributes;
}
}
]
Browser Testing:
- Chrome/Edge (Chromium)
- Firefox
- Safari (especially for CSS grid/flexbox)
WordPress Version Testing:
- Current stable release
- One version back (if supporting older sites)
- Beta/RC (if planning ahead)
Common Pitfalls & Solutions
Pitfall 1: Block Validation Errors
Problem: "This block contains unexpected or invalid content"
Cause: Save function output changed between versions
Solution:
- Use block deprecations when changing save output
- Never change existing attribute sources
- Test migration before deploying
Pitfall 2: Infinite Re-renders
Problem: Block constantly re-renders, editor freezes
Cause: Creating new objects/arrays in render
Solution:
// ❌ Bad - creates new array every render
const items = [];
// ✅ Good - memoize or use state
const [items, setItems] = useState([]);
Pitfall 3: Missing useBlockProps
Problem: Block wrapper styling doesn't work
Cause: Forgot useBlockProps() in edit or save
Solution:
// Always wrap your block
<div {...useBlockProps()}>
{/* content */}
</div>
Pitfall 4: RichText Content Loss
Problem: Content disappears on save
Cause: Missing RichText.Content in save function
Solution:
// edit.js
<RichText
tagName="p"
value={attributes.content}
onChange={(content) => setAttributes({ content })}
/>
// save.js
<RichText.Content tagName="p" value={attributes.content} />
Pitfall 5: Placeholder Styles Not Customized
Problem: Block has unexpected styling that clashes with themes
Cause: Scaffolded style.scss contains placeholder styles
Location: src/style.scss (applies to both editor and frontend)
Solution: Customize or remove the placeholder styles after scaffolding:
// Scaffolded placeholder
.wp-block-my-namespace-my-block {
background-color: #21759b; // Remove or customize
color: #fff; // Remove or customize
padding: 2px;
}
// Customized for production
.wp-block-my-namespace-my-block {
// Only essential structural styles
// Let themes control appearance
}
Remember:
style.scss→ Both frontend + editor (keep minimal)editor.scss→ Editor only (can be more specific)
Pitfall 6: InnerBlocks Content Lost in Dynamic Blocks
Problem: InnerBlocks content disappears after saving in a dynamic block
Cause: Returning null in save.js when using InnerBlocks
Official Guidance: Per the Block Editor Handbook: "If you are using InnerBlocks in a dynamic block you will need to save the InnerBlocks in the save callback function using <InnerBlocks.Content/>"
Solution:
// ❌ WRONG - InnerBlocks content won't persist
export default function save() {
return null;
}
// ✅ CORRECT - Save InnerBlocks even for dynamic blocks
export default function save() {
return (
<div {...useBlockProps.save()}>
<InnerBlocks.Content />
</div>
);
}
Pitfall 7: Fatal Error with Inner Block Rendering
Problem: Fatal error: Cannot use object of type WP_Block as array
Cause: Using wrong rendering method for WP_Block objects
Context: $block->inner_blocks returns an array of WP_Block objects, not arrays.
Solution:
// ❌ WRONG - Causes fatal error
$inner_blocks = $block->inner_blocks;
$output = render_block( $inner_blocks[0] ); // Fatal!
// ✅ CORRECT - Use ->render() method
$inner_blocks = $block->inner_blocks;
$output = $inner_blocks[0]->render();
Reference:
WP_Block::render()- ForWP_Blockobjectsrender_block()- For parsed block arrays
Performance Best Practices
- Minimize attribute updates - Batch
setAttributescalls - Lazy load dependencies - Import heavy libraries only when needed
- Optimize asset loading - Load scripts/styles only on pages using block
- Use block context - Share data between nested blocks efficiently
- Debounce user input - For search/filter controls
Internationalization (i18n)
Always wrap text strings:
import { __ } from '@wordpress/i18n';
const title = __('My Block Title', 'my-text-domain');
const label = _x('Settings', 'block settings label', 'my-text-domain');
const plural = _n('1 item', '%d items', count, 'my-text-domain');
Resources
Official Documentation:
- Block Editor Handbook: https://developer.wordpress.org/block-editor/
- Block Development Examples: https://github.com/WordPress/block-development-examples
- Block API Reference: https://developer.wordpress.org/block-editor/reference-guides/
- Gutenberg Storybook: https://wordpress.github.io/gutenberg/ (interactive component reference)
Learning Platforms:
- Learn WordPress: https://learn.wordpress.org/ (courses on block development)
- WordPress Developer Blog: Latest features and tutorials
Community:
- Make WordPress Slack: #core-editor channel
- GitHub Discussions: WordPress/gutenberg repository
Decision Trees
Should I Build a Custom Block?
Need custom functionality?
├─ No → Use core blocks or block patterns
└─ Yes → Is it reusable across sites?
├─ Yes → Build as plugin (static blocks)
└─ No → Is it theme-specific design?
├─ Yes → Build in theme (dynamic blocks)
└─ No → Consider if block is best solution
Static vs Dynamic?
Will content change frequently without editor intervention?
├─ Yes → Dynamic block
└─ No → Is this for wide distribution?
├─ Yes → Static block
└─ No → Are you comfortable with theme coupling?
├─ Yes → Dynamic block (simpler development)
└─ No → Static block (portable)
Version Notes
- WordPress 6.0+:
apiVersion: 3introduced - WordPress 5.8+: block.json is standard approach
- WordPress 5.5+: InnerBlocks improvements
- Always check compatibility requirements in plugin header
This skill should be used when:
- Creating new custom Gutenberg blocks
- Scaffolding block plugins
- Deciding between static/dynamic rendering
- Setting up TypeScript for block development
- Troubleshooting block validation errors
- Planning block architecture
What ships with it: 1 file
923 B alongside SKILL.md
- README.md923 B