wp-blocks
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.phporrender_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.inputandstyles.elements.select(border, color, outline, shadow, spacing). Focus state not yet supported. - Border radius presets:
settings.border.radiusSizesfor visual selection. - Button pseudo-classes:
:hoverand:focusstates 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-contextancestor.
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 (
hiddennot set server-side). - Ensure PHP and JS derived state logic matches exactly.
3.5 WordPress 6.9 Changes
data-wp-ignoreis 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
attachTofor overlays (modals, pop-ups). - New TypeScript types:
AsyncAction<ReturnType>andTypeYield<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
deprecatedentry with the oldsavefunction and optionally amigratefunction.
Block does not appear in inserter:
- Confirm
block.jsonnameis 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 theinithook.
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
metaattribute 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
apiVersionto3in block.json. The warning only appears whenSCRIPT_DEBUGis true.
Interactivity directives not firing:
- Confirm the
viewScriptModuleis enqueued and loaded (check network tab). - Confirm the DOM element has
data-wp-interactivewith the correct namespace. - Confirm the store namespace matches the directive values.
- Check console for JS errors before hydration.
- Confirm
supports.interactivityis set in block.json (orwp_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 registrationresources/block-themes.md-- full theme.json example, template markup, template parts, pattern docblocks, style variations, template hierarchyresources/interactivity-api.md-- store definitions, SSR render.php examples, derived state closures, non-block usage withwp_interactivity_process_directives(), PHP helper functions, WordPress 6.9 directive changes