WP Blocks, Block Themes, and Interactivity API
Consolidated skill for WordPress Gutenberg block development, block theme creation, and the Interactivity API. Targets WordPress 6.9+ (PHP 7.2.24+).
Part 1: Block Development
1.1 block.json Metadata
Every block starts with a block.json file. WordPress 6.9 enforces apiVersion 3.
{ "$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.", "supports": { "html": false, "color": { "background": true, "text": true }, "typography": { "fontSize": true }, "spacing": { "margin": true, "padding": true } }, "attributes": { "content": { "type": "string", "source": "html", "selector": "p" }, "alignment": { "type": "string", "default": "none" } }, "textdomain": "my-plugin", "editorScript": "file:./index.js", "editorStyle": "file:./index.css", "style": "file:./style-index.css", "render": "file:./render.php", "viewScriptModule": "file:./view.js" }
Required fields: apiVersion , name , title .
Asset fields:
Field Loads in Purpose
editorScript
Editor only Block registration and edit UI
editorStyle
Editor only Editor-specific styles
script
Both Shared JS (editor + frontend)
style
Both Shared styles
viewScript
Frontend only Classic frontend script
viewScriptModule
Frontend only Module-based frontend script (ES)
viewStyle
Frontend only Frontend-only styles
render
Server PHP render file for dynamic blocks
apiVersion 3 migration: Set "apiVersion": 3 , declare all style handles in block.json (missing handles will not load in iframed editor), test third-party scripts (window scoping differs). WordPress 7.0 will always use the iframe editor regardless of apiVersion.
1.2 Scaffolding
npx @wordpress/create-block my-block # Standard block npx @wordpress/create-block my-block --variant dynamic # Dynamic block with render.php npx @wordpress/create-block my-block --template @wordpress/create-block-interactive-template # Interactive
For manual setup: create block.json , register via register_block_type_from_metadata() in PHP, add editor JS and view assets.
1.3 Static vs Dynamic Rendering
Type When to use save() returns
Static Self-contained HTML, no server state dependency Full markup
Dynamic Server data (posts, user info, APIs), must stay current null (or minimal fallback)
-
Static: markup stored in DB; changing save() without deprecation causes "Invalid block" errors.
-
Dynamic: render via render.php or render_callback .
1.4 Wrapper Functions (Required)
Context Function
Editor (edit.js ) useBlockProps()
Static save (save.js ) useBlockProps.save()
Dynamic render (PHP) get_block_wrapper_attributes()
These inject classes, styles, and data attributes generated by block supports. Always spread on the outermost wrapper element.
1.5 Attributes and Serialization
Attributes persist via comment delimiter JSON (default), HTML source (source
- selector ), or context (parent blocks).
Source Description
(none) Stored in block comment delimiter
attribute
Parsed from an HTML attribute (selector
- attribute )
text
Parsed from element text content
html
Parsed from element inner HTML
query
Extracts an array from repeated elements
Rules: Avoid deprecated meta source. Avoid brittle selectors. Never change saved HTML without a deprecated entry.
1.6 InnerBlocks
For container blocks that nest other blocks. Use useInnerBlocksProps() in edit, useInnerBlocksProps.save() in save. Only one InnerBlocks per block. Use templateLock intentionally (false | 'all' | 'insert' | 'contentOnly' ).
1.7 Deprecations
Critical: When you change save() output or attribute shapes, add a deprecation entry. Order newest first. Each entry needs save matching old output. migrate is optional for attribute transforms. Never change save() without a deprecation entry.
1.8 Registration (PHP)
add_action( 'init', function() { register_block_type_from_metadata( DIR . '/build/blocks/my-block' ); } );
For dynamic blocks, pass 'render_callback' as second arg.
Detailed code examples (edit/save patterns, InnerBlocks, block supports, variations, styles, deprecation migrations, PHP registration): see resources/block-development.md
Part 2: Block Themes
2.1 Theme Structure
my-theme/ style.css # Theme header (required) theme.json # Global settings and styles templates/ index.html # Minimum required template single.html page.html archive.html 404.html parts/ header.html footer.html patterns/ hero.php styles/ dark.json # Style variation
2.2 theme.json Structure
theme.json (version 3) defines global settings and styles. Key sections:
Section Purpose
settings.color.palette
Custom color presets
settings.typography.*
Font families, sizes, line height
settings.spacing.*
Units, spacing size presets
settings.layout
contentSize and wideSize
settings.border
Border controls and radius presets
styles.color
Global background/text colors
styles.typography
Global font settings
styles.elements.*
Element styles (link, heading, button, input)
styles.blocks.*
Per-block style overrides
customTemplates
Custom template definitions
templateParts
Template part declarations (header/footer/etc)
Reference presets with var(--wp--preset--<type>--<slug>) .
Style hierarchy: core defaults -> theme.json -> child theme -> user customizations. User customizations stored in DB can override theme.json edits.
2.3 Templates and Template Parts
-
Templates in templates/ use block markup in HTML files.
-
Template parts in parts/ (flat, no subdirectories).
-
Reference parts via <!-- wp:template-part {"slug":"header","area":"header"} /--> .
2.4 Patterns
Filesystem patterns in patterns/*.php are auto-registered. Use docblock headers: Title , Slug , Categories , Keywords , Block Types , Post Types , Viewport Width . Add Inserter: no to hide from inserter.
2.5 Style Variations
JSON files under styles/ override settings and styles. Once a user selects a variation, the choice is stored in the database.
2.6 WordPress 6.9 theme.json Additions
-
Form element styling: styles.elements.input and styles.elements.select (border, color, outline, shadow, spacing). Focus state not yet supported.
-
Border radius presets: settings.border.radiusSizes for visual selection.
-
Button pseudo-classes: :hover and :focus states for Button block directly in theme.json.
Detailed code examples (full theme.json, templates, template parts, patterns, style variations, template hierarchy): see resources/block-themes.md
Part 3: Interactivity API
3.1 Directives Reference
Directive Purpose
data-wp-interactive
Declares an interactive region and namespace
data-wp-context
Provides per-element context (JSON)
data-wp-on--{event}
Attaches synchronous event handler
data-wp-on-async--{event}
Attaches async event handler (preferred)
data-wp-bind--{attr}
Binds a DOM attribute to state/context
data-wp-text
Sets element text content from state/context
data-wp-class--{name}
Toggles a CSS class based on state/context
data-wp-style--{prop}
Sets an inline style property
data-wp-each
Iterates over an array
data-wp-key
Unique key for list items
data-wp-watch
Runs a callback when dependencies change
data-wp-init
Runs once when the element is first connected
data-wp-run
Runs a callback on every render
3.2 Store Pattern
Define stores with store( namespace, { state, actions, callbacks } ) . Use getContext() for per-element context, getElement() for the current DOM ref. Async actions use generator syntax (*fetchData() { yield ... } ).
State vs context:
-
State is global, shared across all instances. Define with store() .
-
Context is per-element, scoped to nearest data-wp-context ancestor.
3.3 Server-Side Rendering (Required)
-
Set "supports": { "interactivity": true } in block.json.
-
Initialize state in PHP with wp_interactivity_state( $ns, $state ) .
-
Output context with wp_interactivity_data_wp_context( $context ) .
-
For themes/plugins without block.json, wrap HTML in wp_interactivity_process_directives() .
PHP helper functions:
Function Purpose
wp_interactivity_state( $ns, $state )
Initialize or get global state for a namespace
wp_interactivity_data_wp_context( $context )
Generate data-wp-context attribute string
wp_interactivity_get_context( $ns )
Get current context during directive processing
wp_interactivity_process_directives( $html )
Manually process directives (themes/plugins)
wp_interactivity_config( $ns, $config )
Set configuration data for a namespace
3.4 Hydration Rules
-
Client JS must produce markup matching server-rendered HTML.
-
Derived state in JS only (not PHP) causes layout shift (hidden not set server-side).
-
Ensure PHP and JS derived state logic matches exactly.
3.5 WordPress 6.9 Changes
-
data-wp-ignore is deprecated. Use conditional rendering or separate interactive regions.
-
Unique directive IDs: Use --- separator for multiple same-type directives on one element.
-
getServerState() / getServerContext() reset between client-side page transitions.
-
Router regions support attachTo for overlays (modals, pop-ups).
-
New TypeScript types: AsyncAction<ReturnType> and TypeYield<T> .
Detailed code examples (store definitions, SSR render.php, derived state closures, non-block usage, 6.9 directive IDs): see resources/interactivity-api.md
Part 4: Tooling
4.1 @wordpress/scripts
npx wp-scripts start # Development build with watch npx wp-scripts build # Production build npx wp-scripts lint-js # Lint JS npx wp-scripts lint-style # Lint CSS npx wp-scripts test-unit-js # Unit tests npx wp-scripts test-e2e # E2E tests
4.2 wp-env
npx wp-env start # Start environment npx wp-env stop # Stop environment npx wp-env run cli wp plugin list # Run WP-CLI commands npx wp-scripts test-e2e # Run E2E tests against the environment
4.3 Debugging Common Issues
"This block contains unexpected or invalid content":
-
You changed save() output or attribute parsing without a deprecation entry.
-
Fix: add a deprecated entry with the old save function and optionally a migrate function.
Block does not appear in inserter:
-
Confirm block.json name is valid and the block is registered.
-
Confirm build output exists and scripts are enqueued.
-
If using PHP registration, confirm register_block_type_from_metadata() runs on the init hook.
Attributes not saving:
-
Confirm attribute definition matches actual markup structure.
-
If the value is in comment delimiter JSON, avoid brittle HTML selectors.
-
Avoid the deprecated meta attribute source.
Styles not applying in editor (apiVersion 3):
-
Ensure style handles are declared in block.json (editorStyle , style ).
-
Styles not declared in block.json will not load inside the iframed editor.
Console warnings about apiVersion (WordPress 6.9+):
- Update apiVersion to 3 in block.json. The warning only appears when SCRIPT_DEBUG is true.
Interactivity directives not firing:
-
Confirm the viewScriptModule is enqueued and loaded (check network tab).
-
Confirm the DOM element has data-wp-interactive with the correct namespace.
-
Confirm the store namespace matches the directive values.
-
Check console for JS errors before hydration.
-
Confirm supports.interactivity is set in block.json (or wp_interactivity_process_directives() is called).
Hydration mismatch / flicker:
-
Server markup differs from client expectations.
-
Derived state not defined in PHP causes missing attributes on initial render.
-
Ensure PHP and JS derived state logic matches.
Part 5: Common Mistakes
Mistake Consequence Fix
Changing save() without deprecation "Invalid block" error on existing posts Add deprecated array entry with old save
Renaming block name in block.json All existing instances break Treat name as immutable stable API
Missing useBlockProps() in edit Block supports (colors, spacing) not applied Always spread useBlockProps() on wrapper
Missing useBlockProps.save() in save Support classes/styles missing from saved markup Always spread on outermost save element
Missing get_block_wrapper_attributes() in PHP render Support classes/styles missing from frontend Always use on wrapper in render.php
Using innerHTML = for block save XSS risk and bypasses sanitization Use proper React components and RichText.Content
Attribute source selector too brittle Attribute value not found after minor markup change Use stable selectors or prefer comment delimiter
apiVersion below 3 Console warnings in 6.9; broken in 7.0 iframe editor Set apiVersion: 3 and test in iframe
Derived state only in JS, not PHP Layout shift on initial load; hidden not set server-side Define matching derived state in PHP with wp_interactivity_state()
Not declaring styles in block.json Styles load on frontend but not in iframed editor Add all handles to editorStyle / style fields
Using data-wp-ignore
Deprecated in 6.9; breaks context and navigation Use conditional rendering or separate regions
Template parts in subdirectories Parts not found by WordPress Keep parts flat in parts/ directory
User customizations overriding theme.json Theme changes appear ignored Check for DB-stored user overrides; reset if needed
Duplicate InnerBlocks in one block Runtime error Only one InnerBlocks per block
templateLock: 'all' without good reason Users cannot modify block content Use sparingly; prefer false or 'insert'
Resources
Detailed code examples and extended references are available in:
-
resources/block-development.md -- edit/save patterns, dynamic rendering, InnerBlocks composition, block supports (full JSON), block variations, block styles, deprecation migrations, PHP registration
-
resources/block-themes.md -- full theme.json example, template markup, template parts, pattern docblocks, style variations, template hierarchy
-
resources/interactivity-api.md -- store definitions, SSR render.php examples, derived state closures, non-block usage with wp_interactivity_process_directives() , PHP helper functions, WordPress 6.9 directive changes