The WordPress Interactivity API gives block developers a Core-supported way to build reactive frontend experiences without turning every block into a separate JavaScript application.
Introduced in WordPress 6.5, the API connects server-rendered HTML to reactive state, local context, actions, and lifecycle callbacks through declarative data-wp-* directives. WordPress Core uses it in blocks including Search, Query, Navigation, and File, so it is no longer an experimental pattern reserved for demos.
This guide explains how the architecture works, builds an accessible disclosure block with correct directive syntax, and covers server directive processing, shared state, asynchronous actions, client-side navigation, security, performance, testing, and enterprise implementation decisions.
Version baseline: The Interactivity API is bundled with WordPress Core from version 6.5. This article follows the current handbook, including Script Modules, client-side navigation guidance, the watch() utility available from WordPress 7.0, and the three-hyphen data-wp-watch---id syntax required before WordPress 7.1.
What the WordPress Interactivity API Solves
The API standardizes frontend interaction for blocks. Markup remains server-rendered, directives declare how elements react, and a namespaced store contains state, actions, callbacks, and derived values. The runtime tracks dependencies and updates only the DOM affected by changed state or context.
Use it for disclosure controls, filters, search, carts, navigation, galleries, forms, and interactions shared across separate blocks. It is particularly useful when a block must remain compatible with WordPress Script Modules and client-side navigation.
Do not confuse the Interactivity API with the WordPress Block Bindings API. Block Bindings maps data into supported block attributes during rendering. The Interactivity API manages reactive frontend state, user events, effects, and navigation after the page has loaded. A platform can use both, but they solve different problems.
Directives, Stores, Context, and Server Rendering
An interactive region starts with data-wp-interactive="namespace". Descendant directives then reference properties in that namespace unless another namespace is specified explicitly.
- Directives connect markup to behaviour. Examples include
data-wp-on--click,data-wp-bind--hidden,data-wp-text,data-wp-class--is-active, and lifecycle directives such asdata-wp-initanddata-wp-watch. - Global state is shared by every interactive region using the namespace. Use it when separate blocks must communicate.
- Local context belongs to one element and its descendants. Use it for independent instances such as multiple accordions or disclosure blocks.
- Derived state calculates a value from state or context through a getter instead of storing duplicate values that can drift out of sync.
- Config contains static, non-reactive values such as endpoints, feature flags, and nonces. It is serialized to the browser and must never contain secrets.
Initial global state should normally be set in PHP with wp_interactivity_state(). Local context can be emitted safely with wp_interactivity_data_wp_context(). WordPress processes supported directives on the server, so the first response can already contain the correct attributes, text, classes, and hidden state before JavaScript runs.
Build an Accessible Interactive Disclosure Block
The official scaffold creates a working interactive block project:
npx @wordpress/create-block@latest interactive-disclosure --template @wordpress/create-block-interactive-template
The template configures the module build automatically. For a manual project, both wp-scripts build and wp-scripts start need the --experimental-modules flag. The broader build pipeline is covered in my guide to modern WordPress asset management with wp-scripts.
block.json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "mg/interactive-disclosure",
"version": "1.0.0",
"title": "Interactive Disclosure",
"category": "widgets",
"icon": "visibility",
"description": "An accessible disclosure powered by the Interactivity API.",
"supports": {
"html": false,
"interactivity": true
},
"attributes": {
"label": {
"type": "string",
"default": "Toggle details"
},
"content": {
"type": "string",
"default": "Disclosure content"
}
},
"render": "file:./render.php",
"viewScriptModule": "file:./view.js"
}
supports.interactivity tells WordPress that the block uses the Interactivity API. viewScriptModule loads the frontend code as a Script Module instead of a traditional script.
render.php
<?php
/**
* Render the Interactive Disclosure block.
*
* @var array $attributes Block attributes.
*/
$label = isset( $attributes['label'] )
? sanitize_text_field( $attributes['label'] )
: __( 'Toggle details', 'mg-interactivity' );
$content = isset( $attributes['content'] )
? $attributes['content']
: __( 'Disclosure content', 'mg-interactivity' );
$panel_id = wp_unique_id( 'mg-disclosure-panel-' );
$context = array(
'isOpen' => false,
);
?>
<div
<?php
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo get_block_wrapper_attributes();
?>
data-wp-interactive="mg/interactive-disclosure"
<?php
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo wp_interactivity_data_wp_context( $context );
?>
>
<button
type="button"
data-wp-on--click="actions.toggle"
data-wp-bind--aria-expanded="context.isOpen"
aria-expanded="false"
aria-controls="<?php echo esc_attr( $panel_id ); ?>"
>
<?php echo esc_html( $label ); ?>
</button>
<div
id="<?php echo esc_attr( $panel_id ); ?>"
data-wp-bind--hidden="!context.isOpen"
hidden
>
<?php echo wp_kses_post( $content ); ?>
</div>
</div>
The disclosure state belongs in local context because every block instance must open independently. The server includes the initial hidden and aria-expanded="false" values, so the first HTML response is correct before hydration. JavaScript enhances the control instead of creating the content from scratch.
view.js
import { getContext, store } from '@wordpress/interactivity';
store( 'mg/interactive-disclosure', {
actions: {
toggle() {
const context = getContext();
context.isOpen = ! context.isOpen;
},
},
} );
The event directive must use data-wp-on--click="actions.toggle". Attribute binding uses data-wp-bind--attribute. Text content uses data-wp-text. Directive values are references to store properties, not arbitrary JavaScript expressions.
Directives You Will Use Most
| Directive | Purpose |
|---|---|
wp-interactive | Defines the active namespace for a region. |
wp-context | Defines reactive local data for an element and its descendants. |
wp-bind | Adds, removes, or changes an HTML attribute. |
wp-text | Updates an element’s text content. |
wp-class | Toggles a class based on a boolean value. |
wp-style | Updates a specific inline style property. |
wp-on | Runs an action for an element event. |
wp-on-window / wp-on-document | Connects actions or callbacks to global events. |
wp-init | Runs setup when an element enters the page. |
wp-watch | Runs a callback and reruns it when referenced reactive data changes. |
wp-run | Runs a callback with hook support during the element lifecycle. |
wp-key, wp-each | Provides stable identity and reactive list rendering. |
When adding multiple watch callbacks to one element, use the three-hyphen form such as data-wp-watch---analytics. The older two-hyphen unique-ID syntax is deprecated and is scheduled to stop working in WordPress 7.1.
Choose the Correct Data Scope
Use local context for independent instances
Menus, accordions, tooltips, and per-card controls normally need local context. Putting an isOpen flag in global state would cause every instance using the namespace to share the same value.
Use global state for cross-block communication
A product block and a cart summary can share one global store even when they are located in separate parts of the page. Initialize server-visible global values with wp_interactivity_state() so server output and client state begin from the same source of truth.
Use derived state instead of duplicated state
import { getContext, store } from '@wordpress/interactivity';
store( 'mg/filter', {
state: {
get hasSelection() {
const { selectedIds } = getContext();
return selectedIds.length > 0;
},
},
} );
A getter stays synchronized automatically. Do not store both selectedIds and a manually updated hasSelection boolean.
Use config for static values
Pass immutable runtime configuration with wp_interactivity_config(). Endpoints, feature flags, and nonces are reasonable examples. Config is visible in the browser, so credentials, private keys, and server secrets never belong there.
Actions, Callbacks, and Asynchronous Work
Event actions run asynchronously by default. Wrap an action with withSyncEvent() when it must call synchronous event APIs such as preventDefault(). For asynchronous actions, generator functions preserve Interactivity API scope across yielded work.
import {
store,
withSyncEvent,
} from '@wordpress/interactivity';
store( 'mg/navigation', {
actions: {
navigate: withSyncEvent( function* ( event ) {
event.preventDefault();
const { actions } = yield import(
'@wordpress/interactivity-router'
);
yield actions.navigate( event.target.href );
} ),
},
} );
Use withScope() when an action relying on getContext() or getElement() is invoked from outside the runtime, such as a timer or external library callback. Use lifecycle callbacks with cleanup functions for observers, timers, subscriptions, and global listeners.
Stores can also be locked to prevent other namespaces from reading or extending private implementation state. This is useful for large plugins where only a small public contract should be available to extenders.
Client-Side Navigation Changes the Integration Rules
The optional @wordpress/interactivity-router can fetch a page, replace matching router regions, manage history, and prefetch destinations. It can make pagination and filters feel immediate, but compatibility must be designed rather than assumed.
- Use Script Modules, not regular scripts or
window.wp.*globals. - Do not initialize blocks with
DOMContentLoaded. Usedata-wp-init,data-wp-watch, or thewatch()utility where appropriate. - Use stable keys and stable selectors across navigations. Query-driven siblings should use
data-wp-key. - Avoid DOM mutations that the Interactivity API cannot reconcile.
- Handle focus and scroll after navigation. The router does not make those product decisions for you.
- Use
getServerState()andgetServerContext()when incoming server values must replace or reconcile existing client values.
Regular jQuery widgets and scripts that run once on page load can break when a router region is replaced. This is one reason correct asset architecture matters. Review how to enqueue scripts in WordPress without breaking production systems before mixing legacy scripts with interactive blocks.
Enterprise Security, Performance, and Accessibility
Treat serialized data as public
State, context, and config sent to the client can be inspected and modified by the user. Never trust client state as authorization. Every mutation endpoint must verify capabilities, validate and sanitize input, and apply nonce or authentication controls appropriate to the request.
Keep the initial HTML truthful
Server and client values should describe the same initial interface. A modal that appears closed in HTML but open in initial state creates layout shifts and confusing assistive-technology output. Render meaningful content and valid ARIA attributes on the server.
Do not serialize an application database
Large state payloads increase HTML size, parsing work, and memory use. Send only the data needed for the current page. Cache expensive server queries, paginate long collections, and fetch additional data only when the interaction requires it.
Build accessibility into the state model
Opening a component often requires more than changing visibility. Menus, dialogs, tabs, and navigation may require keyboard behaviour, focus movement, focus restoration, escape handling, announcements, and reduced-motion support. Directives make attributes reactive, but they do not automatically make an interaction accessible.
Use engineering controls
Apply linting, static analysis, code review, and repeatable builds to interactive code. My guide to enforcing WordPress Coding Standards in team projects provides a baseline for shared repositories. For larger stores, TypeScript support can reduce namespace, context, and asynchronous-action errors during refactoring.
Testing Interactive Blocks
- Verify the initial HTML before JavaScript executes.
- Add end-to-end tests for pointer and keyboard interactions.
- Test multiple instances to detect accidental global-state coupling.
- Test empty, loading, failure, and delayed network states.
- Navigate between different templates when client-side navigation is enabled.
- Check focus order, screen-reader output, reduced motion, and high zoom.
- Measure JavaScript size, interaction latency, layout shifts, and repeated server calls.
- Confirm that cleanup functions remove observers, timers, and subscriptions.
When Not to Use the Interactivity API
The Interactivity API is not required for server-only dynamic content. A dynamic block or Block Bindings may be enough. Editor-only controls should use the block editor’s React and data packages rather than frontend directives.
A standalone application with its own routing, deployment model, and isolated DOM may still justify another framework. The decision should be based on ownership, integration, accessibility, and lifecycle requirements, not on the assumption that every frontend interaction must use one technology.
When the feature belongs to the site’s domain model, package it as a plugin instead of hiding it in a theme. The custom WordPress plugin development guide covers the foundation for maintainable plugin-owned functionality.
Production Implementation Checklist
- Confirm the minimum WordPress version and browser support policy.
- Use local context unless state genuinely needs to be shared.
- Initialize server-visible state and context in PHP.
- Use valid directives and store references, not inline JavaScript expressions.
- Load frontend code through
viewScriptModule. - Design keyboard, focus, ARIA, and failure behaviour before implementation.
- Protect every server mutation independently of client state.
- Use generators,
withSyncEvent(), andwithScope()where their contracts require them. - Test server rendering, hydration, multiple instances, and client navigation.
- Document the public store contract and keep internal stores private.
Keep the official Interactivity API reference, the Directives and Store guide, the server directive processing guide, and the client-side navigation compatibility guide as the source of truth while the API evolves.
Need a Production-Ready Interactive Block System?
I help product teams and agencies design custom blocks, plugin-owned state, accessible interactions, and enterprise WordPress frontend architecture. The implementation must remain predictable across caching, navigation, editorial workflows, accessibility tools, and future WordPress upgrades.
Frequently Asked Questions
Which WordPress version introduced the Interactivity API?
The Interactivity API was introduced in WordPress 6.5. Sites on older WordPress versions need an appropriate Gutenberg version, but production plugins should normally declare and enforce a supported WordPress Core baseline.
Does the Interactivity API replace React?
No. It provides a standardized frontend interaction layer for blocks and uses a reactive runtime. The block editor still uses React-based packages, and standalone applications may have different architectural requirements.
What is the difference between state and context?
Global state is shared across interactive regions using the same namespace. Context is local to an element and its descendants. Use context for independent block instances and state for deliberate cross-block communication.
Is the Interactivity API server-rendered?
WordPress can process supported directives on the server when the block declares interactivity support and its initial state or context is available. Progressive enhancement still depends on the markup and fallback behaviour the developer implements.
Can separate blocks share Interactivity API state?
Yes. Separate blocks can use the same namespace and global state. This supports workflows such as an add-to-cart control updating a cart summary elsewhere on the page. Keep shared state contracts small and documented.
Is the Interactivity API automatically accessible?
No. It makes reactive ARIA attributes and state-driven behaviour easier to implement, but developers remain responsible for keyboard interaction, focus management, semantic HTML, announcements, motion preferences, and testing with assistive technology.





