Engineering Custom Blocks: React, block.json, and the WordPress Data Layer
The definitive guide to the modern WordPress frontend. This article bridges the gap between PHP and React, dissecting the Gutenberg architecture. We cover the full development lifecycle: scaffolding with `@wordpress/scripts`, defining metadata via `block.json`, implementing the Interactivity API, and managing complex application state using Redux-based stores in the WordPress Data Layer.
Block Development Fundamentals
Block Editor Architecture
The Gutenberg editor follows a modular architecture where content is structured as discrete "blocks" managed through a Redux-like state system, with React components handling the UI layer, and a serialization mechanism that converts blocks to HTML comments with JSON attributes for storage in post_content.
┌─────────────────────────────────────────────────────────┐
│ Block Editor │
├──────────────┬──────────────────┬───────────────────────┤
│ Toolbar │ Block Canvas │ Settings Sidebar │
├──────────────┴──────────────────┴───────────────────────┤
│ @wordpress/data (State) │
├─────────────────────────────────────────────────────────┤
│ Block Registration & Parsing │
├─────────────────────────────────────────────────────────┤
│ <!-- wp:block {"attr":value} --> content <!-- /wp --> │
│ (Serialized Storage) │
└─────────────────────────────────────────────────────────┘
React Basics for Blocks
Blocks use React's component model where each block has an Edit component (displayed in editor) and a Save component (generates front-end markup), utilizing hooks like useState and useEffect for local state management, while WordPress-specific hooks like useBlockProps handle block wrapper attributes.
import { useState } from '@wordpress/element'; function Edit({ attributes, setAttributes }) { const [isEditing, setIsEditing] = useState(false); return ( <div {...useBlockProps()}> <p>{attributes.content}</p> </div> ); }
JSX Syntax
JSX is a syntax extension allowing HTML-like markup within JavaScript, which gets transpiled to React.createElement() calls; WordPress uses Babel through @wordpress/scripts to transform JSX, with the key differences from HTML being className instead of class and camelCase attributes.
// JSX Syntax const element = ( <div className="my-block"> <RichText tagName="h2" value={title} onChange={(val) => setAttributes({ title: val })} placeholder="Enter title..." /> </div> ); // Compiles to: React.createElement('div', { className: 'my-block' }, React.createElement(RichText, { tagName: 'h2', value: title, ... }) );
WordPress Packages Overview
WordPress provides 80+ npm packages under @wordpress/* namespace that encapsulate editor functionality, from low-level utilities (@wordpress/dom, @wordpress/url) to complete UI systems (@wordpress/components, @wordpress/block-editor), all maintained in the Gutenberg GitHub repository.
┌─────────────────────────────────────────────────────────────┐
│ @wordpress packages │
├─────────────────┬─────────────────┬─────────────────────────┤
│ Core/Utils │ Components │ Editor │
├─────────────────┼─────────────────┼─────────────────────────┤
│ @wp/element │ @wp/components │ @wp/block-editor │
│ @wp/data │ @wp/primitives │ @wp/editor │
│ @wp/hooks │ @wp/icons │ @wp/blocks │
│ @wp/i18n │ │ @wp/rich-text │
│ @wp/api-fetch │ │ @wp/format-library │
└─────────────────┴─────────────────┴─────────────────────────┘
@wordpress/scripts
This package provides a zero-config build toolchain wrapping webpack, Babel, ESLint, and other tools specifically configured for WordPress development; it handles JSX transformation, SCSS compilation, asset dependencies extraction, and hot module replacement during development.
// package.json { "scripts": { "build": "wp-scripts build", "start": "wp-scripts start", "lint:js": "wp-scripts lint-js", "lint:css": "wp-scripts lint-style", "test": "wp-scripts test-unit-js" }, "devDependencies": { "@wordpress/scripts": "^26.0.0" } }
# Commands npm run build # Production build → build/index.js, index.asset.php npm run start # Development with watch mode
@wordpress/create-block
This scaffolding tool generates a complete block plugin structure with all necessary configuration files, build setup, and example code; it supports interactive mode, templates, and external block templates for customization.
# Create a new block plugin npx @wordpress/create-block my-custom-block # With options npx @wordpress/create-block my-block \ --namespace=myplugin \ --title="My Block" \ --category=widgets \ --variant=dynamic # Generated structure: my-custom-block/ ├── build/ ├── src/ │ ├── block.json │ ├── edit.js │ ├── save.js │ ├── index.js │ ├── editor.scss │ └── style.scss ├── my-custom-block.php └── package.json
Block Registration (JS)
The registerBlockType() function in JavaScript registers a block's client-side behavior including edit and save components, attributes, and UI configuration; with block.json adoption, this function now primarily references the metadata file while adding dynamic JavaScript functionality.
import { registerBlockType } from '@wordpress/blocks'; import { useBlockProps } from '@wordpress/block-editor'; import metadata from './block.json'; import Edit from './edit'; import save from './save'; registerBlockType(metadata.name, { edit: Edit, save, // Can override or extend block.json properties icon: { src: 'smiley', foreground: '#ff0000', }, // Deprecated: attributes, title, etc. now in block.json });
register_block_type() (PHP)
The PHP register_block_type() function registers blocks server-side, typically pointing to block.json for metadata; it handles asset enqueueing, dynamic rendering callbacks, and makes blocks available to the editor through the REST API.
<?php // Modern approach - using block.json function myplugin_register_blocks() { register_block_type( __DIR__ . '/build/my-block' ); } add_action( 'init', 'myplugin_register_blocks' ); // With additional options register_block_type( __DIR__ . '/build/my-block', array( 'render_callback' => 'render_my_block', 'attributes' => array( 'customAttr' => array( 'type' => 'string', 'default' => '', ), ), ) );
block.json
API Version
The apiVersion field indicates which Block API version the block uses, with version 3 (WordPress 6.3+) being current; higher versions enable features like useBlockProps for automatic class handling and improved iframe editor support.
{ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 3 }
API Versions:
├── v1: Legacy (pre-5.6)
├── v2: useBlockProps required, improved serialization (5.6+)
└── v3: Iframe editor support, enhanced context (6.3+)
Name and Title
The name is a unique identifier in namespace/block-name format (lowercase, hyphenated) used programmatically, while title is the human-readable label displayed in the block inserter and must be translatable.
{ "name": "myplugin/testimonial-card", "title": "Testimonial Card" }
Category
The category determines where the block appears in the inserter panel, with core categories being text, media, design, widgets, theme, and embed; custom categories can be registered via PHP using block_categories_all filter.
{ "category": "widgets" }
// Custom category registration add_filter( 'block_categories_all', function( $categories ) { return array_merge( $categories, [[ 'slug' => 'myplugin-blocks', 'title' => 'My Plugin Blocks', 'icon' => 'star-filled', ]]); });
Icon
The icon can be a Dashicons slug string, an SVG string, or a custom object with foreground/background colors; SVGs are preferred for custom icons and must be properly escaped.
{ "icon": "format-quote" }
// Complex icon in JS registration registerBlockType(metadata.name, { icon: { src: <svg viewBox="0 0 24 24">...</svg>, foreground: '#3858e9', background: '#f0f3ff', }, });
Description
The description provides a brief explanation shown in the block inspector sidebar, helping users understand the block's purpose; it should be concise and translatable.
{ "description": "Display customer testimonials with photo and rating." }
Keywords
The keywords array contains up to three terms that help users find the block when searching in the inserter; they supplement the title and should include common synonyms or related concepts.
{ "keywords": ["quote", "review", "feedback"] }
Attributes
The attributes object defines the data schema for block content, specifying types, sources for parsing from saved HTML, and default values; this enables structured data storage within serialized block comments.
{ "attributes": { "content": { "type": "string", "source": "html", "selector": "p" }, "alignment": { "type": "string", "default": "left" }, "mediaId": { "type": "number" }, "items": { "type": "array", "default": [] } } }
Supports
The supports object enables or disables built-in block features like alignment, colors, typography, and spacing without additional code; WordPress automatically handles UI, CSS generation, and attribute storage for enabled features.
{ "supports": { "align": ["wide", "full"], "anchor": true, "color": { "background": true, "text": true, "gradients": true }, "spacing": { "margin": true, "padding": true }, "typography": { "fontSize": true, "lineHeight": true } } }
Styles
The styles array defines visual variations of a block that users can select from the block toolbar or sidebar; each style adds a corresponding is-style-{name} class to the block wrapper.
{ "styles": [ { "name": "default", "label": "Default", "isDefault": true }, { "name": "rounded", "label": "Rounded Corners" }, { "name": "outlined", "label": "Outlined" } ] }
editorScript and editorStyle
The editorScript specifies the JavaScript file loaded only in the editor (block registration, Edit component), while editorStyle loads CSS that applies exclusively to the editor view, typically for admin-specific styling.
{ "editorScript": "file:./index.js", "editorStyle": "file:./index.css" }
Loading Context:
┌─────────────┬──────────────────┬─────────────────┐
│ Asset │ Editor │ Frontend │
├─────────────┼──────────────────┼─────────────────┤
│editorScript │ ✓ │ ✗ │
│editorStyle │ ✓ │ ✗ │
│script │ ✓ │ ✓ │
│style │ ✓ │ ✓ │
│viewScript │ ✗ │ ✓ │
└─────────────┴──────────────────┴─────────────────┘
script and style
The script loads JavaScript in both editor and frontend contexts (shared functionality), while style applies CSS universally to ensure consistent block appearance across editing and viewing.
{ "script": "file:./shared.js", "style": "file:./style-index.css" }
viewScript
The viewScript loads JavaScript only on the frontend when the block is rendered, ideal for interactive features like carousels, accordions, or any client-side behavior not needed during editing.
{ "viewScript": "file:./view.js" }
// src/view.js - Frontend only document.querySelectorAll('.wp-block-myplugin-accordion').forEach(el => { el.querySelector('.accordion-header').addEventListener('click', () => { el.classList.toggle('is-open'); }); });
render
The render property specifies a PHP file for server-side block rendering, replacing inline render_callback; this file receives $attributes, $content, and $block variables and should return the block's HTML.
{ "render": "file:./render.php" }
<?php // render.php - Available variables: $attributes, $content, $block $wrapper_attributes = get_block_wrapper_attributes([ 'class' => 'custom-class' ]); ?> <div <?php echo $wrapper_attributes; ?>> <h3><?php echo esc_html( $attributes['title'] ); ?></h3> <?php echo $content; // InnerBlocks content ?> </div>
Block Attributes
Attribute Types
Attribute types define the data format stored in block attributes, supporting string, number, boolean, object, array, and null; the type determines validation and serialization behavior when blocks are saved.
{ "attributes": { "title": { "type": "string" }, "count": { "type": "number" }, "isEnabled": { "type": "boolean" }, "settings": { "type": "object" }, "items": { "type": "array" }, "optionalField": { "type": ["string", "null"] } } }
Attribute Sources
Sources define how attribute values are parsed from saved HTML content, enabling semantic HTML storage; types include html (innerHTML), text (textContent), attribute (HTML attribute), and query (multiple elements).
{ "attributes": { "content": { "type": "string", "source": "html", "selector": "p" }, "url": { "type": "string", "source": "attribute", "selector": "a", "attribute": "href" }, "links": { "type": "array", "source": "query", "selector": "li", "query": { "text": { "source": "text" }, "url": { "source": "attribute", "selector": "a", "attribute": "href" } } } } }
Saved HTML → Attribute Parsing:
<div class="wp-block-myplugin-example">
<p>Hello World</p> → content: "Hello World"
<a href="/page">Link</a> → url: "/page"
</div>
Default Values
The default property sets initial attribute values when a block is inserted; defaults should match the attribute type and are used when no value is sourced from saved content or block comment.
{ "attributes": { "alignment": { "type": "string", "default": "center" }, "columns": { "type": "number", "default": 3 }, "features": { "type": "array", "default": ["feature1", "feature2"] } } }
Selectors
Selectors in attribute definitions specify which HTML element to extract data from using CSS selector syntax; they work with source to identify the exact DOM node for parsing, supporting complex nested structures.
{ "attributes": { "heading": { "type": "string", "source": "html", "selector": ".card-title h2" }, "imageAlt": { "type": "string", "source": "attribute", "selector": "figure img", "attribute": "alt" } } }
Attribute Serialization
Attributes are serialized as JSON in HTML comments (<!-- wp:block {"attr":"value"} -->), with source-based attributes extracted from inner HTML; understanding serialization is crucial for debugging and custom parsing scenarios.
<!-- Serialized block in post_content --> <!-- wp:myplugin/card {"backgroundColor":"#fff","columns":3} --> <div class="wp-block-myplugin-card"> <h2>Card Title</h2> <!-- Sourced: title attribute --> <p>Card content here</p> <!-- Sourced: content attribute --> </div> <!-- /wp:myplugin/card -->
┌─────────────────────────────────────────────┐
│ Attribute Storage │
├─────────────────────────────────────────────┤
│ With source: Extracted from HTML content │
│ Without source: Stored in JSON comment │
└─────────────────────────────────────────────┘
Meta Attributes
Meta attributes store values in post meta instead of block content, enabling data access outside the editor; they require source: "meta" and the meta key must be registered with show_in_rest enabled.
{ "attributes": { "subtitle": { "type": "string", "source": "meta", "meta": "custom_subtitle" } } }
// Register meta field register_post_meta( 'post', 'custom_subtitle', array( 'show_in_rest' => true, 'single' => true, 'type' => 'string', 'auth_callback' => function() { return current_user_can( 'edit_posts' ); }, ) );
Block Components
Edit Function
The Edit function is a React component that defines the block's editor interface, receiving attributes, setAttributes, isSelected, and other props; it should return JSX wrapped with useBlockProps() for proper editor integration.
import { useBlockProps } from '@wordpress/block-editor'; export default function Edit({ attributes, setAttributes, isSelected }) { const blockProps = useBlockProps({ className: `align-${attributes.alignment}` }); return ( <div {...blockProps}> {isSelected && <p>Block is selected!</p>} <p>{attributes.content || 'Click to edit...'}</p> </div> ); }
Save Function
The Save function returns static HTML markup stored in the database, running only during save and not on page load; it receives only attributes and must be deterministic—returning null indicates a dynamic block with server-side rendering.
import { useBlockProps } from '@wordpress/block-editor'; export default function save({ attributes }) { const blockProps = useBlockProps.save({ className: `align-${attributes.alignment}` }); return ( <div {...blockProps}> <p>{attributes.content}</p> </div> ); } // For dynamic blocks: export default function save() { return null; // Server renders via render_callback }
RichText Component
RichText provides a contenteditable field with formatting toolbar support for bold, italic, links, and custom formats; it handles attribute storage and supports multiple HTML tag outputs through the tagName prop.
import { RichText, useBlockProps } from '@wordpress/block-editor'; function Edit({ attributes, setAttributes }) { return ( <div {...useBlockProps()}> <RichText tagName="h2" value={attributes.heading} onChange={(heading) => setAttributes({ heading })} placeholder="Enter heading..." allowedFormats={['core/bold', 'core/italic', 'core/link']} /> </div> ); } // In save: <RichText.Content tagName="h2" value={attributes.heading} />
MediaUpload
MediaUpload opens the WordPress media library modal for file selection, providing flexibility in UI through render props; it handles media selection and returns attachment data including ID, URL, and metadata.
import { MediaUpload, MediaUploadCheck } from '@wordpress/block-editor'; import { Button } from '@wordpress/components'; <MediaUploadCheck> <MediaUpload onSelect={(media) => setAttributes({ mediaId: media.id, mediaUrl: media.url })} allowedTypes={['image']} value={attributes.mediaId} render={({ open }) => ( <Button onClick={open} variant="primary"> {attributes.mediaUrl ? 'Replace Image' : 'Upload Image'} </Button> )} /> </MediaUploadCheck>
MediaPlaceholder
MediaPlaceholder displays a styled upload area with drag-and-drop support, URL input, and media library access; it's ideal for blocks where media is the primary content and provides a polished initial state.
import { MediaPlaceholder, useBlockProps } from '@wordpress/block-editor'; function Edit({ attributes, setAttributes }) { if (!attributes.mediaUrl) { return ( <div {...useBlockProps()}> <MediaPlaceholder icon="format-image" labels={{ title: 'Image', instructions: 'Upload or drag image' }} onSelect={(media) => setAttributes({ mediaId: media.id, mediaUrl: media.url })} accept="image/*" allowedTypes={['image']} /> </div> ); } return <div {...useBlockProps()}><img src={attributes.mediaUrl} /></div>; }
InspectorControls
InspectorControls renders content in the block settings sidebar (right panel) using the SlotFill pattern; it's used for secondary settings that don't require inline editing, keeping the canvas clean.
import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; import { PanelBody, RangeControl, ToggleControl } from '@wordpress/components'; function Edit({ attributes, setAttributes }) { return ( <> <InspectorControls> <PanelBody title="Settings" initialOpen={true}> <RangeControl label="Columns" value={attributes.columns} onChange={(columns) => setAttributes({ columns })} min={1} max={6} /> <ToggleControl label="Show Border" checked={attributes.showBorder} onChange={(showBorder) => setAttributes({ showBorder })} /> </PanelBody> </InspectorControls> <div {...useBlockProps()}>Block content</div> </> ); }
BlockControls
BlockControls adds toolbar buttons and dropdowns to the block toolbar that appears above selected blocks; it's used for frequently-accessed formatting options like alignment, list styles, or quick actions.
import { BlockControls, useBlockProps, AlignmentToolbar } from '@wordpress/block-editor'; import { ToolbarGroup, ToolbarButton } from '@wordpress/components'; function Edit({ attributes, setAttributes }) { return ( <> <BlockControls> <AlignmentToolbar value={attributes.alignment} onChange={(alignment) => setAttributes({ alignment })} /> <ToolbarGroup> <ToolbarButton icon="edit" label="Edit" onClick={() => setAttributes({ isEditing: true })} /> </ToolbarGroup> </BlockControls> <div {...useBlockProps()}>Content</div> </> ); }
Toolbar Controls
Toolbar controls provide various interactive elements for the block toolbar including ToolbarButton, ToolbarGroup, ToolbarDropdownMenu, and specialized controls like AlignmentToolbar; they enable quick access to block actions and formatting.
import { BlockControls } from '@wordpress/block-editor'; import { ToolbarGroup, ToolbarButton, ToolbarDropdownMenu } from '@wordpress/components'; import { formatBold, formatItalic, more } from '@wordpress/icons'; <BlockControls> <ToolbarGroup> <ToolbarButton icon={formatBold} label="Bold" isPressed={attributes.isBold} onClick={() => setAttributes({ isBold: !attributes.isBold })} /> <ToolbarDropdownMenu icon={more} label="More Options" controls={[ { title: 'Option 1', icon: formatBold, onClick: () => {} }, { title: 'Option 2', icon: formatItalic, onClick: () => {} }, ]} /> </ToolbarGroup> </BlockControls>
PanelBody
PanelBody creates collapsible sections within InspectorControls, organizing settings into logical groups; it supports initial open/closed state, icons, and can be programmatically controlled.
import { PanelBody, PanelRow } from '@wordpress/components'; <InspectorControls> <PanelBody title="Layout Options" initialOpen={true} icon="layout"> <PanelRow> <p>Settings go here</p> </PanelRow> </PanelBody> <PanelBody title="Advanced" initialOpen={false}> {/* More settings */} </PanelBody> </InspectorControls>
TextControl
TextControl is a standard text input field for single-line string values, commonly used in the sidebar for titles, URLs, CSS classes, and other simple text attributes.
import { TextControl } from '@wordpress/components'; <TextControl label="Button Text" value={attributes.buttonText} onChange={(buttonText) => setAttributes({ buttonText })} help="Text displayed on the button" placeholder="Click here" />
SelectControl
SelectControl renders a dropdown menu for selecting from predefined options, supporting single selection with optional grouped options and help text; use for enumerated choices like sizes, positions, or categories.
import { SelectControl } from '@wordpress/components'; <SelectControl label="Size" value={attributes.size} options={[ { label: 'Small', value: 'small' }, { label: 'Medium', value: 'medium' }, { label: 'Large', value: 'large' }, ]} onChange={(size) => setAttributes({ size })} />
ToggleControl
ToggleControl provides an on/off switch for boolean attributes, displaying a label and optional help text; it's preferred over checkboxes in WordPress UI for feature toggles and settings.
import { ToggleControl } from '@wordpress/components'; <ToggleControl label="Enable Animation" checked={attributes.hasAnimation} onChange={(hasAnimation) => setAttributes({ hasAnimation })} help={attributes.hasAnimation ? 'Animation is enabled' : 'Animation is disabled'} />
RangeControl
RangeControl displays a slider for numeric values within a specified range, supporting step increments, marks, and before/after icons; ideal for columns, spacing, opacity, or any bounded numeric setting.
import { RangeControl } from '@wordpress/components'; <RangeControl label="Opacity" value={attributes.opacity} onChange={(opacity) => setAttributes({ opacity })} min={0} max={100} step={10} marks={[ { value: 0, label: '0%' }, { value: 50, label: '50%' }, { value: 100, label: '100%' }, ]} withInputField={true} />
ColorPicker
ColorPicker provides a full-featured color selection interface with hue/saturation picker, hex input, and alpha channel support; it's typically used in custom UI implementations requiring complete color control.
import { ColorPicker, Popover, Button } from '@wordpress/components'; import { useState } from '@wordpress/element'; function ColorControl({ value, onChange }) { const [isOpen, setIsOpen] = useState(false); return ( <> <Button onClick={() => setIsOpen(true)} style={{ backgroundColor: value }}> Select Color </Button> {isOpen && ( <Popover onClose={() => setIsOpen(false)}> <ColorPicker color={value} onChange={onChange} enableAlpha /> </Popover> )} </> ); }
ColorPalette
ColorPalette displays a preset swatch grid for color selection, integrating with theme color settings and supporting custom colors; it provides a simpler, more constrained UI than ColorPicker for theme-consistent designs.
import { ColorPalette } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; function Edit({ attributes, setAttributes }) { const colors = useSelect((select) => select('core/block-editor').getSettings().colors ); return ( <ColorPalette colors={colors} value={attributes.backgroundColor} onChange={(backgroundColor) => setAttributes({ backgroundColor })} clearable={true} /> ); }
InnerBlocks
InnerBlocks Usage
InnerBlocks enables nesting blocks within a parent block, creating container-type blocks like columns, groups, or cards; the parent defines the wrapper while children handle individual content pieces.
import { InnerBlocks, useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; function Edit() { const blockProps = useBlockProps(); const innerBlocksProps = useInnerBlocksProps(blockProps, { // InnerBlocks configuration }); return <div {...innerBlocksProps} />; } function save() { return ( <div {...useBlockProps.save()}> <InnerBlocks.Content /> </div> ); }
Allowed Blocks
The allowedBlocks prop restricts which block types can be inserted as children, ensuring content structure integrity; passing an empty array prevents any blocks, while omitting it allows all blocks.
<InnerBlocks allowedBlocks={[ 'core/paragraph', 'core/heading', 'core/image', 'myplugin/custom-block' ]} /> // Allow only specific core blocks const ALLOWED_BLOCKS = ['core/paragraph', 'core/list']; <InnerBlocks allowedBlocks={ALLOWED_BLOCKS} />
Template
The template prop defines an initial block structure inserted when the parent block is added, providing a starting layout; it's an array of arrays where each inner array specifies block name, attributes, and nested inner blocks.
const TEMPLATE = [ ['core/heading', { level: 2, placeholder: 'Card Title' }], ['core/paragraph', { placeholder: 'Card description...' }], ['core/buttons', {}, [ ['core/button', { text: 'Learn More' }] ]] ]; <InnerBlocks template={TEMPLATE} />
Template Structure:
['block/name', { attributes }, [ nested blocks ]]
Example renders:
┌────────────────────────────┐
│ [Heading: Card Title] │
│ [Paragraph: Description] │
│ [Button: Learn More] │
└────────────────────────────┘
Template Lock
The templateLock prop controls whether users can modify the inner blocks structure; "all" prevents adding, removing, or moving blocks, "insert" prevents adding/removing but allows reordering, and false (default) allows full editing.
// Fully locked - exact structure required <InnerBlocks template={TEMPLATE} templateLock="all" /> // Can't add/remove, but can reorder <InnerBlocks template={TEMPLATE} templateLock="insert" /> // Can't add/remove, but can move and edit <InnerBlocks template={TEMPLATE} templateLock="contentOnly" />
templateLock values:
├── "all" → No add/remove/move
├── "insert" → No add/remove, can move
├── "contentOnly" → Structure locked, content editable
└── false → Fully editable (default)
Orientation
The orientation prop hints to the editor whether inner blocks flow horizontally or vertically, affecting insertion indicators, block mover UI, and keyboard navigation; valid values are "horizontal" and "vertical" (default).
// Horizontal layout (like columns) <InnerBlocks orientation="horizontal" template={[ ['core/column', {}], ['core/column', {}], ['core/column', {}], ]} /> // CSS should match orientation // .wp-block-myplugin-row { display: flex; flex-direction: row; }
Dynamic Blocks
Server-Side Rendering
Dynamic blocks generate HTML on each page load via PHP instead of storing static markup, enabling content that depends on current context, database queries, or external data; the save function returns null indicating no client-side HTML storage.
// edit.js function Edit({ attributes }) { return <div {...useBlockProps()}>Editor preview (may differ from frontend)</div>; } // save.js export default function save() { return null; // No static HTML - PHP renders on each request }
Static Block: [Save] → HTML in DB → Display HTML
Dynamic Block: [Save] → null in DB → PHP renders → Display HTML
render_callback
The render_callback is a PHP function specified during block registration that generates the block's frontend HTML; it receives $attributes, $content, and $block object parameters for complete rendering control.
register_block_type('myplugin/recent-posts', array( 'render_callback' => 'myplugin_render_recent_posts', )); function myplugin_render_recent_posts($attributes, $content, $block) { $posts = get_posts(array( 'numberposts' => $attributes['count'] ?? 5, 'post_status' => 'publish', )); $output = '<ul class="recent-posts">'; foreach ($posts as $post) { $output .= sprintf( '<li><a href="%s">%s</a></li>', get_permalink($post), esc_html($post->post_title) ); } $output .= '</ul>'; return $output; }
PHP Block Rendering
Modern block.json supports a render property pointing to a PHP template file, which is cleaner than inline callbacks; the template receives $attributes, $content, and $block as variables and should return/echo the HTML.
{ "render": "file:./render.php" }
<?php // render.php $count = $attributes['count'] ?? 5; $wrapper_attributes = get_block_wrapper_attributes(['class' => 'recent-posts']); ?> <div <?php echo $wrapper_attributes; ?>> <h3><?php echo esc_html($attributes['title']); ?></h3> <ul> <?php $posts = get_posts(['numberposts' => $count]); foreach ($posts as $post) : ?> <li><a href="<?php echo get_permalink($post); ?>"> <?php echo esc_html($post->post_title); ?> </a></li> <?php endforeach; ?> </ul> </div>
Block Context
Block context allows parent blocks to share data with deeply nested children without prop drilling; it's a mechanism similar to React Context where ancestor blocks expose values and descendants can consume them.
Parent Block (provides context)
├── Child Block
│ └── Grandchild Block (uses context)
└── Another Child (uses context)
Context Flow: Parent → All Descendants
Uses Context
The usesContext property in block.json declares which context values from ancestors a block wants to access; these values are then available in the block's render callback or Edit component via the context prop.
{ "name": "myplugin/post-title", "usesContext": ["postId", "postType", "queryId"] }
// render.php - context values available $post_id = $block->context['postId']; $title = get_the_title($post_id);
// edit.js function Edit({ context }) { const { postId, postType } = context; // Use context values }
Provides Context
The providesContext property in block.json exposes attribute values to descendant blocks, mapping attribute names to context keys; this enables building connected block systems like query loops or product grids.
{ "name": "myplugin/post-container", "attributes": { "selectedPostId": { "type": "number" } }, "providesContext": { "myplugin/postId": "selectedPostId" } }
// Child block uses this context { "name": "myplugin/post-meta", "usesContext": ["myplugin/postId"] }
Block Supports
align
The align support enables block alignment options (left, center, right, wide, full), automatically adding alignment classes and UI controls; it can be a boolean or array specifying allowed alignments.
{ "supports": { "align": true, // All alignments "align": ["wide", "full"], // Only wide and full "align": ["left", "center", "right"] } }
/* Generated classes */ .alignleft { float: left; } .aligncenter { margin-left: auto; margin-right: auto; } .alignwide { max-width: var(--wp--style--global--wide-size); } .alignfull { max-width: none; width: 100%; }
anchor
The anchor support adds an HTML ID field in the Advanced panel, allowing users to create link targets; the ID is automatically applied to the block wrapper element.
{ "supports": { "anchor": true } }
<!-- User enters "my-section" as anchor --> <div id="my-section" class="wp-block-myplugin-card">...</div> <!-- Can be linked to: --> <a href="#my-section">Jump to section</a>
color
The color support enables text, background, and gradient controls in the sidebar, with WordPress handling CSS variable generation and class application; granular control allows enabling specific color features.
{ "supports": { "color": { "text": true, "background": true, "gradients": true, "link": true, "__experimentalDefaultControls": { "background": true, "text": true } } } }
<!-- Generated output --> <div class="wp-block-myplugin-card has-background has-primary-background-color" style="background-color:var(--wp--preset--color--primary)">
spacing
The spacing support adds margin and padding controls that integrate with theme spacing presets, generating appropriate CSS custom properties and inline styles.
{ "supports": { "spacing": { "margin": true, "padding": true, "blockGap": true } } }
<!-- Generated output with user spacing --> <div class="wp-block-myplugin-card" style="margin-top:var(--wp--preset--spacing--40); padding:var(--wp--preset--spacing--20)">
typography
The typography support enables font size, line height, font family, font weight, letter spacing, and text decoration controls that use theme.json typography presets.
{ "supports": { "typography": { "fontSize": true, "lineHeight": true, "fontFamily": true, "fontWeight": true, "fontStyle": true, "textTransform": true, "textDecoration": true, "letterSpacing": true } } }
customClassName
The customClassName support (enabled by default) adds an Additional CSS Class field in the Advanced panel; set to false to remove this option for blocks where custom classes shouldn't be applied.
{ "supports": { "customClassName": true, // Default "customClassName": false // Remove the field } }
<!-- User adds "my-custom-class" --> <div class="wp-block-myplugin-card my-custom-class">
html
The html support (enabled by default) allows the block to be edited as raw HTML; disable it for complex blocks where HTML editing would break functionality or cause validation errors.
{ "supports": { "html": false // Disable "Edit as HTML" option } }
inserter
The inserter support controls whether the block appears in the block inserter; set to false to hide blocks that should only be inserted programmatically or as inner blocks of specific parents.
{ "supports": { "inserter": false // Hidden from inserter, can still be used programmatically } }
multiple
The multiple support (enabled by default) determines if multiple instances of the block can exist in a post; set to false for singleton blocks like "Cover Image" or "Featured Post" that should appear only once.
{ "supports": { "multiple": false // Only one instance allowed per post } }
reusable
The reusable support (enabled by default) allows the block to be converted to a reusable block (synced pattern); disable for blocks with post-specific content that shouldn't be reused.
{ "supports": { "reusable": false // Cannot be converted to reusable block } }
className
The className support (enabled by default) automatically adds the wp-block-{namespace}-{name} class to the block wrapper; disable only if you're handling class names entirely manually.
{ "supports": { "className": true // Adds wp-block-myplugin-card (default) "className": false // No automatic class } }
Advanced Block Development
Block Transforms
Transforms define how blocks can convert to/from other block types, enabling operations like converting a paragraph to a heading or a list to multiple paragraphs; they support from and to directions with various transform types.
import { registerBlockType } from '@wordpress/blocks'; registerBlockType('myplugin/callout', { transforms: { from: [ { type: 'block', blocks: ['core/paragraph'], transform: ({ content }) => { return createBlock('myplugin/callout', { content }); }, }, { type: 'enter', // Transform on Enter key regExp: /^!!$/, transform: () => createBlock('myplugin/callout'), }, ], to: [ { type: 'block', blocks: ['core/paragraph'], transform: ({ content }) => { return createBlock('core/paragraph', { content }); }, }, ], }, });
Block Variations
Variations create alternative configurations of existing blocks with preset attributes, inner blocks, and different names/icons; they're lighter than creating separate blocks and useful for semantic variants like "Wide Quote" or "Full-Width Image."
import { registerBlockVariation } from '@wordpress/blocks'; registerBlockVariation('core/group', { name: 'card', title: 'Card', description: 'A card-styled group block', icon: 'id-alt', attributes: { className: 'is-style-card', backgroundColor: 'white', }, innerBlocks: [ ['core/heading', { level: 3 }], ['core/paragraph'], ], scope: ['inserter', 'block', 'transform'], isActive: (blockAttributes) => blockAttributes.className?.includes('is-style-card'), });
Block Styles
Block styles register visual variations that add a CSS class (.is-style-{name}) without changing block structure or attributes; users select styles from the toolbar or sidebar, and CSS handles the visual differences.
import { registerBlockStyle } from '@wordpress/blocks'; registerBlockStyle('core/quote', { name: 'fancy', label: 'Fancy Quote', }); registerBlockStyle('core/button', [ { name: 'gradient', label: 'Gradient' }, { name: 'outline-thick', label: 'Thick Outline' }, ]);
/* styles.css */ .wp-block-quote.is-style-fancy { border-left: 4px solid gold; background: linear-gradient(to right, #fff9e6, transparent); font-style: italic; }
Block Patterns Programmatic
Block patterns are predefined block arrangements registered via PHP, offering users ready-made layouts; they combine multiple blocks with preset content and attributes, appearing in the inserter pattern tab.
register_block_pattern( 'myplugin/hero-section', array( 'title' => __( 'Hero Section', 'myplugin' ), 'description' => __( 'A hero section with heading and buttons.', 'myplugin' ), 'categories' => array( 'featured', 'header' ), 'keywords' => array( 'hero', 'banner' ), 'content' => ' <!-- wp:cover {"overlayColor":"primary","minHeight":500} --> <div class="wp-block-cover" style="min-height:500px"> <div class="wp-block-cover__inner-container"> <!-- wp:heading {"textAlign":"center","level":1} --> <h1 class="has-text-align-center">Welcome</h1> <!-- /wp:heading --> <!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"}} --> <div class="wp-block-buttons"> <!-- wp:button --> <div class="wp-block-button"><a class="wp-block-button__link">Get Started</a></div> <!-- /wp:button --> </div> <!-- /wp:buttons --> </div> </div> <!-- /wp:cover --> ', ) );
Block Bindings
Block bindings (WordPress 6.5+) connect block attributes to dynamic data sources like post meta, site options, or custom sources; this enables blocks to display dynamic content without server-side rendering.
{ "supports": { "interactivity": true } }
<!-- Block with bindings --> <!-- wp:paragraph { "metadata":{ "bindings":{ "content":{ "source":"core/post-meta", "args":{"key":"custom_field"} } } } } --> <p></p> <!-- /wp:paragraph -->
// Register custom binding source register_block_bindings_source('myplugin/user-data', array( 'label' => 'User Data', 'get_value_callback' => function($args) { return get_user_meta(get_current_user_id(), $args['key'], true); }, ));
Block Hooks
Block hooks (WordPress 6.5+) allow blocks to automatically inject themselves relative to other blocks at specific positions (before, after, firstChild, lastChild); this enables extensibility without modifying templates.
{ "name": "myplugin/like-button", "blockHooks": { "core/post-content": "after", "core/post-title": "after" } }
// Conditional hook with PHP add_filter('hooked_block_types', function($hooked_blocks, $position, $anchor_block) { if ($anchor_block === 'core/post-content' && $position === 'after') { if (is_single()) { $hooked_blocks[] = 'myplugin/share-buttons'; } } return $hooked_blocks; }, 10, 3);
Interactivity API
The Interactivity API (WordPress 6.5+) provides a standardized way to add frontend interactivity to blocks using directives (data-wp-* attributes), replacing custom JavaScript with a declarative, Vue-like approach.
{ "supports": { "interactivity": true }, "viewScriptModule": "file:./view.js" }
<!-- render.php --> <div <?php echo get_block_wrapper_attributes(); ?> data-wp-interactive="myplugin" data-wp-context='{"isOpen": false}' > <button data-wp-on--click="actions.toggle"> <span data-wp-text="state.buttonText"></span> </button> <div data-wp-bind--hidden="!context.isOpen"> Content here </div> </div>
// view.js import { store } from '@wordpress/interactivity'; store('myplugin', { state: { get buttonText() { return state.isOpen ? 'Close' : 'Open'; } }, actions: { toggle() { const context = getContext(); context.isOpen = !context.isOpen; } } });
Block Deprecation
Deprecations define migration paths when block markup changes between versions, preventing validation errors for existing content; they specify old attributes, save functions, and optional migration functions.
registerBlockType('myplugin/card', { // Current version save: ({ attributes }) => ( <div className="card"><p>{attributes.content}</p></div> ), deprecated: [ { // Version 1: Used 'text' attribute, now 'content' attributes: { text: { type: 'string' } }, save: ({ attributes }) => ( <div class="old-card">{attributes.text}</div> ), migrate: (attributes) => ({ content: attributes.text, }), }, ], });
Deprecation Flow:
Saved HTML → Try current save → Fail? → Try deprecated[0] → Match? → Migrate → Resave
Block Migration
Migration functions within deprecations transform old attribute structures to new formats, running automatically when deprecated blocks are detected and re-saved; they receive old attributes and inner blocks, returning updated values.
deprecated: [ { attributes: { imageUrl: { type: 'string' }, imageAlt: { type: 'string' }, }, migrate: (attributes, innerBlocks) => { // Transform flat attributes to nested object return [ { media: { url: attributes.imageUrl, alt: attributes.imageAlt, }, // Keep other attributes content: attributes.content, }, innerBlocks, // Pass through unchanged ]; }, save: oldSaveFunction, }, ]
WordPress Data Layer
@wordpress/data
The @wordpress/data package is WordPress's state management solution built on Redux principles, providing a centralized store system for editor data; it enables blocks and plugins to access, modify, and subscribe to application state predictably.
┌─────────────────────────────────────────────────────┐
│ @wordpress/data Architecture │
├─────────────────────────────────────────────────────┤
│ Components ←→ Selectors ←→ Store ←→ Actions │
│ ↓ │
│ Reducers │
│ ↓ │
│ State Tree │
└─────────────────────────────────────────────────────┘
useSelect
The useSelect hook retrieves data from stores reactively, re-rendering the component when selected data changes; it receives a callback with the select function and an optional dependencies array for optimization.
import { useSelect } from '@wordpress/data'; function MyComponent() { const { postTitle, categories, isPublished } = useSelect((select) => { const editor = select('core/editor'); return { postTitle: editor.getEditedPostAttribute('title'), categories: editor.getEditedPostAttribute('categories'), isPublished: editor.isCurrentPostPublished(), }; }, []); return <p>Title: {postTitle}</p>; }
useDispatch
The useDispatch hook returns action dispatchers for a specified store, enabling components to modify state; it's memoized and doesn't cause re-renders, making it safe to call unconditionally.
import { useDispatch } from '@wordpress/data'; import { Button } from '@wordpress/components'; function SaveButton() { const { savePost, editPost } = useDispatch('core/editor'); const { createNotice } = useDispatch('core/notices'); const handleSave = async () => { await savePost(); createNotice('success', 'Post saved!', { type: 'snackbar' }); }; return <Button onClick={handleSave}>Save</Button>; }
createReduxStore
The createReduxStore function creates a custom data store with defined reducer, actions, selectors, and optional controls for side effects; it returns a store descriptor that must be registered with register().
import { createReduxStore, register } from '@wordpress/data'; const DEFAULT_STATE = { items: [], isLoading: false }; const store = createReduxStore('myplugin/data', { reducer(state = DEFAULT_STATE, action) { switch (action.type) { case 'SET_ITEMS': return { ...state, items: action.items }; case 'SET_LOADING': return { ...state, isLoading: action.isLoading }; default: return state; } }, actions: { setItems: (items) => ({ type: 'SET_ITEMS', items }), setLoading: (isLoading) => ({ type: 'SET_LOADING', isLoading }), }, selectors: { getItems: (state) => state.items, isLoading: (state) => state.isLoading, }, }); register(store);
Data Stores Overview
WordPress provides multiple core stores managing different aspects of the editor: core for entities and REST API data, core/editor for current post editing, core/block-editor for block manipulation, and core/notices for user notifications.
┌─────────────────────────────────────────────────────────┐
│ Core Data Stores │
├──────────────────┬──────────────────────────────────────┤
│ core │ Entities, REST API, site data │
│ core/editor │ Current post, meta, save status │
│ core/block-editor│ Blocks, selection, insertion │
│ core/blocks │ Block types registry │
│ core/notices │ Admin notices and snackbars │
│ core/preferences │ User preferences │
│ core/viewport │ Responsive breakpoints │
└──────────────────┴──────────────────────────────────────┘
core/editor Store
The core/editor store manages the current post being edited, providing access to post attributes, saving status, undo history, and post-level operations; it's the primary store for post-related block functionality.
import { useSelect, useDispatch } from '@wordpress/data'; function PostInfo() { const { title, status, isSaving, isDirty } = useSelect((select) => ({ title: select('core/editor').getEditedPostAttribute('title'), status: select('core/editor').getEditedPostAttribute('status'), isSaving: select('core/editor').isSavingPost(), isDirty: select('core/editor').isEditedPostDirty(), })); const { editPost, savePost, undo, redo } = useDispatch('core/editor'); return ( <div> <p>{title} ({status}) {isDirty && '*'}</p> <button onClick={() => editPost({ title: 'New Title' })}>Change Title</button> </div> ); }
core/blocks Store
The core/blocks store maintains the registry of all block types, categories, styles, and variations; use it to query available blocks, check block support features, or dynamically register block assets.
import { useSelect } from '@wordpress/data'; function BlockExplorer() { const { blockTypes, categories } = useSelect((select) => ({ blockTypes: select('core/blocks').getBlockTypes(), categories: select('core/blocks').getCategories(), })); const imageBlock = useSelect((select) => select('core/blocks').getBlockType('core/image') ); console.log(imageBlock.supports); // { align: true, anchor: true, ... } }
core/block-editor Store
The core/block-editor store handles block-level operations including current selection, block list manipulation, insertion, and settings; it's essential for building tools that interact with blocks programmatically.
import { useSelect, useDispatch } from '@wordpress/data'; function BlockTools() { const { selectedBlock, blocks } = useSelect((select) => ({ selectedBlock: select('core/block-editor').getSelectedBlock(), blocks: select('core/block-editor').getBlocks(), })); const { insertBlock, removeBlock, updateBlockAttributes } = useDispatch('core/block-editor'); const addParagraph = () => { const newBlock = wp.blocks.createBlock('core/paragraph', { content: 'Hello!' }); insertBlock(newBlock); }; return ( <div> <p>Selected: {selectedBlock?.name}</p> <button onClick={addParagraph}>Add Paragraph</button> </div> ); }
Custom Stores
Custom stores encapsulate plugin-specific state management, separating concerns from component logic; they support async controls for API calls, resolvers for on-demand data fetching, and full Redux middleware integration.
import { createReduxStore, register } from '@wordpress/data'; import apiFetch from '@wordpress/api-fetch'; const store = createReduxStore('myplugin/settings', { reducer(state = { settings: {}, loaded: false }, action) { switch (action.type) { case 'RECEIVE_SETTINGS': return { ...state, settings: action.settings, loaded: true }; default: return state; } }, actions: { receiveSettings: (settings) => ({ type: 'RECEIVE_SETTINGS', settings }), *fetchSettings() { const settings = yield apiFetch({ path: '/myplugin/v1/settings' }); return { type: 'RECEIVE_SETTINGS', settings }; }, }, selectors: { getSettings: (state) => state.settings, isLoaded: (state) => state.loaded, }, resolvers: { *getSettings() { const settings = yield apiFetch({ path: '/myplugin/v1/settings' }); return { type: 'RECEIVE_SETTINGS', settings }; }, }, }); register(store);
SlotFill System
Slot and Fill Concept
SlotFill is a component pattern enabling distant parts of the React tree to render content into designated locations; Slot defines where content appears, Fill provides the content, and WordPress uses this for extensibility throughout the editor.
┌────────────────────────────────────────────┐
│ Editor Component Tree │
│ ├── Header │
│ │ └── <Slot name="toolbar" /> │◄─┐
│ ├── Sidebar │ │
│ │ └── <Slot name="settings" /> │ │ Fill content
│ └── YourBlock │ │ renders here
│ └── <Fill name="toolbar"> │──┘
│ <button>Custom</button> │
│ </Fill> │
└────────────────────────────────────────────┘
import { createSlotFill } from '@wordpress/components'; const { Fill, Slot } = createSlotFill('MyPluginSlot'); // In your layout component function Layout() { return ( <div> <header><Slot /></header> <main>Content</main> </div> ); } // Anywhere else in the app function MyFeature() { return <Fill><button>Injected Button</button></Fill>; }
PluginSidebar
PluginSidebar adds a custom panel to the editor sidebar (alongside Document/Block settings), accessible via a toolbar icon; it's ideal for plugin-wide settings, custom tools, or additional metadata interfaces.
import { PluginSidebar, PluginSidebarMoreMenuItem } from '@wordpress/edit-post'; import { registerPlugin } from '@wordpress/plugins'; import { PanelBody, TextControl } from '@wordpress/components'; registerPlugin('myplugin-sidebar', { render: () => ( <> <PluginSidebarMoreMenuItem target="myplugin-sidebar"> My Plugin Settings </PluginSidebarMoreMenuItem> <PluginSidebar name="myplugin-sidebar" title="My Plugin" icon="admin-plugins" > <PanelBody title="Settings"> <TextControl label="API Key" /> </PanelBody> </PluginSidebar> </> ), });
PluginDocumentSettingPanel
PluginDocumentSettingPanel adds a panel directly within the Document settings sidebar tab, integrating seamlessly with core post settings like Status, Categories, and Featured Image.
import { PluginDocumentSettingPanel } from '@wordpress/edit-post'; import { registerPlugin } from '@wordpress/plugins'; import { TextControl, ToggleControl } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; registerPlugin('myplugin-document-panel', { render: () => { const meta = useSelect((select) => select('core/editor').getEditedPostAttribute('meta') ); const { editPost } = useDispatch('core/editor'); return ( <PluginDocumentSettingPanel name="myplugin-settings" title="My Plugin Settings" className="myplugin-panel" > <TextControl label="Subtitle" value={meta?.myplugin_subtitle || ''} onChange={(value) => editPost({ meta: { myplugin_subtitle: value } })} /> </PluginDocumentSettingPanel> ); }, });
PluginPostStatusInfo
PluginPostStatusInfo injects content into the Post Status section (Summary panel) of the Document sidebar, useful for adding quick-access status indicators, metrics, or actions near publish controls.
import { PluginPostStatusInfo } from '@wordpress/edit-post'; import { registerPlugin } from '@wordpress/plugins'; registerPlugin('myplugin-post-status', { render: () => ( <PluginPostStatusInfo className="myplugin-status"> <div style={{ display: 'flex', justifyContent: 'space-between' }}> <span>Word Count:</span> <strong>1,234</strong> </div> <div style={{ display: 'flex', justifyContent: 'space-between' }}> <span>Reading Time:</span> <strong>5 min</strong> </div> </PluginPostStatusInfo> ), });
PluginPrePublishPanel
PluginPrePublishPanel adds a panel to the pre-publish checklist that appears when users click "Publish," enabling final validation checks, confirmations, or required actions before publishing.
import { PluginPrePublishPanel } from '@wordpress/edit-post'; import { registerPlugin } from '@wordpress/plugins'; import { CheckboxControl } from '@wordpress/components'; import { useState } from '@wordpress/element'; registerPlugin('myplugin-prepublish', { render: () => { const [confirmed, setConfirmed] = useState(false); return ( <PluginPrePublishPanel title="Publishing Checklist" initialOpen={true} > <CheckboxControl label="I have reviewed this post for errors" checked={confirmed} onChange={setConfirmed} /> <CheckboxControl label="Featured image is set" checked={true} disabled /> <CheckboxControl label="SEO meta is configured" checked={false} disabled /> </PluginPrePublishPanel> ); }, });
PluginBlockSettingsMenuItem
PluginBlockSettingsMenuItem adds a menu item to the block's "More options" menu (⋮), enabling custom actions for specific or all block types with access to the selected block's data.
import { PluginBlockSettingsMenuItem } from '@wordpress/edit-post'; import { registerPlugin } from '@wordpress/plugins'; import { useSelect } from '@wordpress/data'; registerPlugin('myplugin-block-action', { render: () => { const selectedBlock = useSelect((select) => select('core/block-editor').getSelectedBlock() ); return ( <PluginBlockSettingsMenuItem allowedBlocks={['core/paragraph', 'core/heading']} icon="analytics" label="Analyze Content" onClick={() => { console.log('Analyzing:', selectedBlock?.attributes?.content); // Perform analysis }} /> ); }, });
Custom Slot/Fill
Creating custom SlotFill pairs enables plugin extensibility, allowing other plugins or themes to inject content into designated areas; this pattern is how WordPress core exposes extension points throughout the editor.
import { createSlotFill, SlotFillProvider } from '@wordpress/components'; // Create a named slot/fill pair const { Fill: MyPluginFill, Slot: MyPluginSlot } = createSlotFill('MyPluginExtension'); // Export for other plugins to use export { MyPluginFill }; // In your plugin's main component function MyPlugin() { return ( <div className="myplugin-wrapper"> <h2>My Plugin</h2> <MyPluginSlot fillProps={{ version: '1.0' }}> {(fills) => fills.length > 0 ? fills : <p>No extensions</p>} </MyPluginSlot> </div> ); } // Third-party extension import { MyPluginFill } from 'myplugin'; function ThirdPartyExtension() { return ( <MyPluginFill> {({ version }) => <div>Extension for v{version}</div>} </MyPluginFill> ); }
Custom SlotFill Flow:
┌─────────────────────────────────────────┐
│ Your Plugin │
│ ├── Creates SlotFill: 'MyPluginExt' │
│ └── Renders <Slot /> in UI │
├─────────────────────────────────────────┤
│ Third-Party Plugin │
│ └── Imports Fill, renders <Fill> │
│ └── Content appears in your Slot │
└─────────────────────────────────────────┘