In previous posts on this blog, we’ve explored various facets of WordPress block development, including both static and dynamic blocks, as well as ways to enhance core block functionality. Up to now, our approach focused mainly on standard blocks that didn’t respond immediately to user input—in other words, non-interactive blocks.

In this article, we’re shifting our attention to a new method for building blocks—one that allows you to incorporate real-time interactivity by harnessing the capabilities of the WordPress Interactivity API. Introduced in WordPress 6.5, this robust API lets you create blocks that respond directly to user actions, paving the way for engaging, dynamic, and feature-rich experiences across your website.

There’s plenty to cover, so let’s begin by reviewing the essentials you’ll need to get started!

What you need before you start with the Interactivity API

Because the Interactivity API is built on React, you should have at least a foundational understanding of both React and server-side JavaScript. Familiarity with build tools like npm and npx, as well as solid experience with WordPress development and the Gutenberg block editor, is essential.

With those skills in your toolkit, set up a local development environment to speed up WordPress project launches. Choose any tool that enables quick and flexible WordPress setup for local development and testing—this will streamline your workflow and give you the flexibility to experiment confidently.

When setting up a local WordPress project, consider these configuration options:

  • Top Level Domain (e.g., .local by default)
  • Preferred PHP version
  • Your chosen database name
  • HTTPS support
  • WordPress site details
  • Automatic WordPress updates
  • Multisite options

Many local development tools also let you import existing WordPress sites from backups for easy migration.

Configuring a local website in DevKinsta
Configuring a local website in DevKinsta

What is the Interactivity API?

The Interactivity API is a WordPress-native solution designed to bring real-time interactivity to your Gutenberg blocks—and, by extension, your entire site. It offers a lightweight, modern approach grounded in declarative programming to handle user interactions efficiently.

Developing interactive blocks from scratch does demand advanced PHP and server-side JavaScript skills. Fortunately, WordPress provides a ready-to-use template for interactive blocks, so you don’t need to start from the ground up every time:

npx @wordpress/create-block --template @wordpress/create-block-interactive-template

This template comes with everything you need for your first interactive block, including sample buttons for toggling the current theme and expanding/collapsing content. These examples provide a solid reference point as you embark on your own projects.

To begin, use your preferred command line tool to navigate to your local WordPress installation’s Plugins directory. Run the following command:

npx @wordpress/create-block your-interactive-block --template @wordpress/create-block-interactive-template

When the installation finishes, open your project folder in your code editor of choice. Visual Studio Code is popular, but any modern editor will do.

An interactive block in Visual Studio Code
The interactive block project provided by the @wordpress/create-block-interactive-template

Next, from your terminal, head to the new plugin’s directory and start the development server:

npm start

With the development server running, you’ll see immediate updates to your block with every change you make.

In your WordPress admin dashboard, go to the Plugins page and activate the Interactivity API plugin you just created. Add a new post or page, locate Your interactive block in the block inserter, and add it to the content. Save and preview on the frontend: you’ll see a yellow block with two buttons—one toggles the block’s background color, the other shows or hides a paragraph.

An example interactive block
An example interactive block provided by the @wordpress/create-block-interactive-template

With this starter plugin in place, you’re ready to dive deeper and discover what makes interactive blocks tick.

The structure of interactive blocks

Interactive blocks share much of their structure with traditional Gutenberg blocks. You’ll work with files like package.json, block.json, edit.js, and style.scss. In addition, you’ll need a render.php file for server-side rendering and a view.js file for managing interactivity on the frontend.

Here’s a breakdown of each key file in the interactive block starter template.

package.json

The package.json file identifies your Node.js project, manages scripts, and handles dependencies essential for development.

Consider the package.json from the create-block-interactive-template as an example:

{
	"name": "your-interactive-block",
	"version": "0.1.0",
	"description": "An interactive block with the Interactivity API.",
	"author": "The WordPress Contributors",
	"license": "GPL-2.0-or-later",
	"main": "build/index.js",
	"scripts": {
		"build": "wp-scripts build --experimental-modules",
		"format": "wp-scripts format",
		"lint:css": "wp-scripts lint-style",
		"lint:js": "wp-scripts lint-js",
		"packages-update": "wp-scripts packages-update",
		"plugin-zip": "wp-scripts plugin-zip",
		"start": "wp-scripts start --experimental-modules"
	},
	"dependencies": {
		"@wordpress/interactivity": "latest"
	},
	"files": [
		"[^.]*"
	],
	"devDependencies": {
		"@wordpress/scripts": "^30.24.0"
	}
}

The scripts and dependencies sections are particularly relevant here.

  • build: Compiles the source code into production-ready JavaScript. --experimental-modules enables WordPress script modules.
  • start: Launches the development server with module support enabled.
  • dependencies: Ensures the Interactivity API package is included in your runtime dependencies.

block.json

The block.json manifest sets out metadata, media, scripts, and styling for your Gutenberg block. By default, the create-block-interactive-template provides a setup like this:

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "create-block/your-interactive-block",
	"version": "0.1.0",
	"title": "Your Interactive Block",
	"category": "widgets",
	"icon": "media-interactive",
	"description": "An interactive block with the Interactivity API.",
	"example": {},
	"supports": {
		"interactivity": true
	},
	"textdomain": "your-interactive-block",
	"editorScript": "file:./index.js",
	"editorStyle": "file:./index.css",
	"style": "file:./style-index.css",
	"render": "file:./render.php",
	"viewScriptModule": "file:./view.js"
}

Key elements in this file include:

  • apiVersion: Version 3 unlocks the latest block features, including script modules support.
  • supports: Dictates block capabilities. Setting "interactivity": true is required for Interactivity API support.
  • render: Points to the PHP file that handles server-side rendering and block interactivity directives.
  • viewScriptModule: Specifies the JavaScript file containing interactive logic, loaded only on relevant pages and only when the interactive block is present.

render.php

Within render.php, you construct the dynamic block’s markup. Making a block interactive involves adding the correct attributes to the DOM elements.

Here’s an example of the render.php file from the sample project:

<?php
/**
 * PHP file to use when rendering the block type on the server to show on the front end.
 *
 * The following variables are exposed to the file:
 *     $attributes (array): The block attributes.
 *     $content (string): The block default content.
 *     $block (WP_Block): The block instance.
 *
 * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render
 */

// Generates a unique id for aria-controls.
$unique_id = wp_unique_id( 'p-' );

// Adds the global state.
wp_interactivity_state(
	'create-block',
	array(
		'isDark'    => false,
		'darkText'  => esc_html__( 'Switch to Light', 'your-interactive-block' ),
		'lightText' => esc_html__( 'Switch to Dark', 'your-interactive-block' ),
		'themeText'	=> esc_html__( 'Switch to Dark', 'your-interactive-block' ),
	)
);
?>

<div
	<?php echo get_block_wrapper_attributes(); ?>
	data-wp-interactive="create-block"
	<?php echo wp_interactivity_data_wp_context( array( 'isOpen' => false ) ); ?>
	data-wp-watch="callbacks.logIsOpen"
	data-wp-class--dark-theme="state.isDark"
>
	<button
		data-wp-on--click="actions.toggleTheme"
		data-wp-text="state.themeText"
	></button>

	<button
		data-wp-on--click="actions.toggleOpen"
		data-wp-bind--aria-expanded="context.isOpen"
		aria-controls="<?php echo esc_attr( $unique_id ); ?>"
	>
		<?php esc_html_e( 'Toggle', 'your-interactive-block' ); ?>
	</button>

	<p
		id="<?php echo esc_attr( $unique_id ); ?>"
		data-wp-bind--hidden="!context.isOpen"
	>
		<?php
			esc_html_e( 'Your Interactive Block - hello from an interactive block!', 'your-interactive-block' );
		?>
	</p>
</div>

Let’s walk through what’s happening in this file:

  • wp_interactivity_state: Initializes or retrieves the global state for an Interactivity API “store.”
  • data-wp-interactive: Activates the Interactivity API for a DOM element and its children—use your unique namespace.
  • wp_interactivity_data_wp_context(): Generates the data-wp-context directive, defining a local state for the node and its descendants.
  • data-wp-watch: Calls a callback on node creation and whenever context or state updates.
  • data-wp-class--dark-theme: Dynamically adds/removes the dark-theme class.
  • data-wp-on--click: Executes logic on click events.
  • data-wp-text: Sets the inner text of a DOM element.
  • data-wp-bind--aria-expanded and data-wp-bind--hidden: Dynamically set HTML attributes based on reactive values.

view.js

This file creates the Store for your block, managing state, actions, and event-driven callbacks.

Here’s what the starter view.js file looks like:

/**
 * WordPress dependencies
 */
import { store, getContext } from '@wordpress/interactivity';

const { state } = store( 'create-block', {
	state: {
		get themeText() {
			return state.isDark ? state.darkText : state.lightText;
		},
	},
	actions: {
		toggleOpen() {
			const context = getContext();
			context.isOpen = ! context.isOpen;
		},
		toggleTheme() {
			state.isDark = ! state.isDark;
		},
	},
	callbacks: {
		logIsOpen: () => {
			const { isOpen } = getContext();
			// Log the value of `isOpen` each time it changes.
			console.log( `Is open: ${ isOpen }` );
		},
	},
} );
  • store: The core function for establishing and registering both global state and logic.
  • getContext: Accesses localized state for the element that initiated an event.
  • state: Holds global reactive data for the block.
  • actions: Methods for modifying state in response to events.
  • callbacks: Functions that run automatically in response to specific changes or triggers.

This might seem complex at first, but we’ll break down each concept as we go along.

Next up: a look at directives, state management, actions, and callbacks within the Interactivity API.

Interactivity API directives

Similar to leading frontend frameworks like Alpine.js and Vue.js, the Interactivity API leverages special HTML attributes—known as directives—to allow your markup to respond to events, update state, change the DOM, and handle user input.

Directives connect your template to the underlying JavaScript, making interactivity possible and easy to manage.

Some of the most commonly used directives include:

FunctionDirectiveDescription
Activation/Namespacedata-wp-interactiveEnables the Interactivity API on this element and its children. Assign your plugin’s unique identifier here.
Local statedata-wp-contextAssigns local “context” state to the current element and descendants. Use a JSON object—consider leveraging wp_interactivity_data_wp_context() for this in PHP (e.g., render.php).
Attribute Bindingdata-wp-bind--[attribute]Sets a given HTML attribute dynamically (e.g., disabled, value) from a reactive value.
Text Modificationdata-wp-textChanges the element’s inner text—must be a string.
CSS Class Togglingdata-wp-class--[classname]Adds or removes a CSS class based on a Boolean reactive value.
Inline stylingdata-wp-style--[css-property]Dynamically sets inline styles in response to changes.
Event Handlingdata-wp-on--[event]Executes code for standard DOM events (like click, mouseover).
Initial Executiondata-wp-initRuns a callback one time—when the DOM node is created.
State Watchingdata-wp-watchTriggers a callback both when a node is initialized and when its state/context updates.
List Iterationdata-wp-eachLoops through and renders repeated elements.

Consult the Interactivity API dev notes and API reference for the full set of supported directives.

Global state, local context, and derived state

Understanding how the Interactivity API handles state management is fundamental before you dive in. If you’ve worked with frameworks like React, Vue, or Angular, concepts like state and context will sound familiar. Here’s a quick refresher for anyone new to these ideas.

Global state

Global state is accessible from nearly everywhere in your app. In the Interactivity API, global state ensures all interactive blocks on a page can share and synchronize information. For example, adding a product to a cart can update both the product block and the shopping cart block at once.

Set initial global state values with the wp_interactivity_state() function in your server-side code. In the starter project, this call appears in render.php like this:

// Adds the global state.
wp_interactivity_state(
	'create-block',
	array(
		'isDark'    => false,
		'darkText'  => esc_html__( 'Switch to Light', 'your-interactive-block' ),
		'lightText' => esc_html__( 'Switch to Dark', 'your-interactive-block' ),
		'themeText'	=> esc_html__( 'Switch to Dark', 'your-interactive-block' ),
	)
);

This function accepts:

  • A unique namespace to identify your store (such as create-block).
  • An array containing the data to merge into your store’s namespace.

The initial values populate your page rendering. To reference them directly, use the state property in your directives, like so:

<button
	data-wp-on--click="actions.toggleTheme"
	data-wp-text="state.themeText"
></button>

In your JavaScript, leverage the store() function to read or write to the global state in that namespace. An example from the starter project’s view.js file:

import { store, getContext } from '@wordpress/interactivity';

const { state } = store( 'create-block', {
	state: { ... },
	actions: { ... },
	callbacks: { ... },
} );

To retrieve part of the state, simply use the state object:

actions: {
	toggleTheme() {
		state.isDark = ! state.isDark;
	},
},

Local context

Local context is data kept private to a particular component and its direct descendants. Interactive WordPress blocks use independent state for the block and its nested elements, helping keep data changes limited to their intended scope.

Call the getContext() function to access the Local context in the Interactivity API. In the starter project, after the toggle button is clicked, the toggleOpen() action flips a value in the local context like this:

actions: {
	toggleOpen() {
		const context = getContext();
		context.isOpen = ! context.isOpen;
	},
},
  • getContext(): Retrieves local state (or “context”) for the triggered node. The fields available correspond to what you set in wp_interactivity_data_wp_context() in render.php.
  • context.isOpen = ! context.isOpen;: Updates the isOpen property for that block’s local context.

Derived state

Derived state refers to values calculated (not directly stored) from existing state or context. It’s recalculated whenever its dependencies update.

Take a look at the view.js file’s section for derived state:

const { state } = store( 'create-block', {
	state: {
		get themeText() {
			return state.isDark ? state.darkText : state.lightText;
		},
	},
	...
}

Here, themeText is computed based on state.isDark. Every time isDark changes, themeText automatically returns either state.darkText or state.lightText, as appropriate.

  • The get themeText() pattern doesn’t require you to call it as a function. The API automatically updates the computed property when its dependencies shift, simplifying your logic and UI updates.

To dive deeper, check out Understanding global state, local context and derived state for more background.

Actions and callbacks

Actions and callbacks dictate how your block responds to user actions or state changes.

The actions section in your block’s code contains functions that run in response to user events and are primarily used to update state or context. Refer to the following excerpt from view.js:

actions: {
	toggleOpen() {
		const context = getContext();
		context.isOpen = ! context.isOpen;
	},
	...
},
  • The toggleOpen() function here uses getContext() to retrieve local context and switch the isOpen value.

You can reach into global state just as easily:

actions: {
	...,
	toggleTheme() {
		state.isDark = ! state.isDark;
	},
},
  • The toggleTheme() function modifies the global state (specifically, toggling the isDark setting).

Actions are linked to UI events through directives like data-wp-on--[event]. For example, see this snippet from render.php:

<button
	data-wp-on--click="actions.toggleOpen"
	data-wp-bind--aria-expanded="context.isOpen"
	aria-controls="<?php echo esc_attr( $unique_id ); ?>"
>
  • Here, data-wp-on--click ensures the toggleOpen action fires when the user clicks the button.

The callbacks section handles functions that run automatically whenever relevant state or context changes, allowing you to produce side effects such as logging or alerting the user.

For instance, the template includes this callback:

callbacks: {
	logIsOpen: () => {
		const { isOpen } = getContext();
		// Log the value of `isOpen` each time it changes.
		console.log( `Is open: ${ isOpen }` );
	},
},
  • The logIsOpen function observes the isOpen value and logs a message every time it changes.
  • This is achieved by calling getContext() inside the callback.
A message in the console informs the user of the change in the Local context.
A message in the console informs the user of the change in the Local context.

How to build an interactive block

Now that you’ve got the theoretical underpinnings, let’s dive into a hands-on example. The next section walks through the creation of an interactive block that lets users add products to a demo shopping basket, complete with real-time updates on quantities and totals. This sample block will give you practical insight into working with state, actions, and callbacks.

The interactive block in the editor
The interactive block in the editor

We’ll build a block called Interactive Counter using the create-block-interactive-template. Begin by running this command in your CLI:

npx @wordpress/create-block interactive-counter --template @wordpress/create-block-interactive-template

Next, change into the new project’s directory and run an initial build.

cd interactive-counter && npm run build

With your project open in your code editor, locate the /src/block.json file. Expect it to look similar to this:

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "create-block/interactive-counter",
	"version": "0.1.0",
	"title": "Interactive Counter",
	"category": "widgets",
	"icon": "media-interactive",
	"description": "An interactive block with the Interactivity API.",
	"supports": {
		"interactivity": true
	},
	"textdomain": "interactive-counter",
	"editorScript": "file:./index.js",
	"editorStyle": "file:./index.css",
	"style": "file:./style-index.css",
	"render": "file:./render.php",
	"viewScriptModule": "file:./view.js"
}

Modify the fields to suit your needs but keep the required fields as detailed above unchanged.

The edit.js file

The next step is creating the interface you’ll see within the editor. Edit the /src/edit.js file as shown below:

import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import './editor.scss';

export default function Edit({ attributes, setAttributes }) {
	const blockProps = useBlockProps();
	const products = [
		{ id: 'product1', name: __('Product 1', 'interactive-counter'), price: 10.00 },
		{ id: 'product2', name: __('Product 2', 'interactive-counter'), price: 15.00 },
		{ id: 'product3', name: __('Product 3', 'interactive-counter'), price: 20.00 },
	];

	return (
		<div {...blockProps}>
			<h3>{__('Shopping Cart', 'interactive-counter')}</h3>
			<ul>
				{products.map((product) => (
					<li key={product.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
						<span style={{ flex: 1 }}>{product.name} - ${product.price.toFixed(2)}</span>
						<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
							<button disabled>-</button>
							<span>0</span>
							<button disabled>+</button>
						</div>
						<span style={{ flex: 1, textAlign: 'right' }}>
							{__('Subtotal:', 'interactive-counter')} $0.00
						</span>
					</li>
				))}
			</ul>
			<div style={{ borderTop: '1px solid #ccc', paddingTop: '15px' }}>
				<p style={{ display: 'flex', justifyContent: 'space-between' }}>
					<strong>{__('Subtotal:', 'interactive-counter')}</strong>
					<span>$0.00</span>
				</p>
				<p style={{ display: 'flex', justifyContent: 'space-between' }}>
					<strong>{__('Tax (22%):', 'interactive-counter')}</strong>
					<span>$0.00</span>
				</p>
				<p style={{ display: 'flex', justifyContent: 'space-between' }}>
					<strong>{__('Total:', 'interactive-counter')}</strong>
					<span>$0.00</span>
				</p>
			</div>
			<p>{__('Quantities and totals will be interactive in the frontend.', 'interactive-counter')}</p>
		</div>
	);
}

This sets up your custom backend block—remember, its interactivity is only visible on the frontend. See our Gutenberg block development guides for more on customizing /src/edit.js.

The render.php file

Next, open /src/render.php and replace its content with:

<?php
/**
 * Render callback for the interactive-counter block.
 */

$products = [
	['id' => 'product1', 'name' => __('Product 1', 'interactive-counter'), 'price' => 10.00],
	['id' => 'product2', 'name' => __('Product 2', 'interactive-counter'), 'price' => 15.00],
	['id' => 'product3', 'name' => __('Product 3', 'interactive-counter'), 'price' => 20.00],
];

// Initialize global state
wp_interactivity_state('interactive-counter', [
	'products' => array_map(function ($product) {
		return [
			'id' => $product['id'],
			'name' => $product['name'],
			'price' => $product['price'],
			'quantity' => 0,
			'subtotal' => '0.00',
		];
	}, $products),
	'vatRate' => 0.22,
]);

Here’s what’s happening in this file:

  • An array of products is hard-coded, supplying each with an ID, name, and price.
  • The wp_interactivity_state call sets up the initial global store state; ensure the namespace matches what’s in view.js.
  • You map the products array to add quantity and subtotal fields, creating a useful structure for block interactions.
  • vatRate provides a default tax rate for calculations.

Now, add this portion to include block markup and directives:

<div <?php echo get_block_wrapper_attributes(); ?> data-wp-interactive="interactive-counter" data-wp-init="callbacks.init">
	<h3><?php echo esc_html__('Cart', 'interactive-counter'); ?></h3>
	<ul>
		<?php foreach ($products as $index => $product) : ?>
			<li data-wp-context='{
				"productId": "<?php echo esc_attr($product['id']); ?>",
				"quantity": 0,
				"subtotal": "0.00"
			}' 
			data-wp-bind--data-wp-context.quantity="state.products[<?php echo $index; ?>].quantity" 
			data-wp-bind--data-wp-context.subtotal="state.products[<?php echo $index; ?>].subtotal">
				<span class="product-name"><?php echo esc_html($product['name']); ?> - 
lt;?php echo esc_html(number_format($product['price'], 2)); ?></span>
				<div class="quantity-controls">
					<button data-wp-on--click="actions.decrement">-</button>
					<span data-wp-text="context.quantity">0</span>
					<button data-wp-on--click="actions.increment">+</button>
				</div>
				<span class="product-subtotal">
					<?php echo esc_html__('Subtotal:', 'interactive-counter'); ?>
					
lt;span data-wp-text="context.subtotal">0.00</span>
				</span>
			</li>
		<?php endforeach; ?>
	</ul>
	<div class="totals">
		<p>
			<strong><?php echo esc_html__('Subtotal:', 'interactive-counter'); ?></strong>
			$ <span data-wp-text="state.subtotal">0.00</span>
		</p>
		<p>
			<strong><?php echo esc_html__('Tax (22%):', 'interactive-counter'); ?></strong>
			$ <span data-wp-text="state.vat">0.00</span>
		</p>
		<p>
			<strong><?php echo esc_html__('Total:', 'interactive-counter'); ?></strong>
			$ <span data-wp-text="state.total">0.00</span>
		</p>
	</div>
</div>

Here’s a closer look at its function:

  • get_block_wrapper_attributes() inserts standard WordPress block classes (e.g. wp-block-create-block-interactive-counter).
  • data-wp-interactive activates frontend interactivity.
  • data-wp-init triggers the corresponding init callback defined in view.js.
  • The foreach loop builds a list for each product defined in the products array.
  • data-wp-context specifies the local context for the block’s logic.
  • data-wp-bind links context.quantity to state.products[$index].quantity, keeping them in sync.
  • Likewise, other binds sync subtotals and related data.
  • The decrement and increment actions are attached to their respective buttons through data-wp-on--click.
  • data-wp-text ensures button and span text updates instantly with context values.

The rest of the markup follows similar logic, letting you focus on interactive behaviors.

The view.js file

This is where your block’s logic lives, managing state, interactivity, and event handling.

import { store, getContext } from '@wordpress/interactivity';

store('interactive-counter', {
	state: {
		get subtotal() {
			const { products } = store('interactive-counter').state;
			return products
				.reduce((sum, product) => sum + product.price * (product.quantity || 0), 0)
				.toFixed(2);
		},
		get vat() {
			const { subtotal, vatRate } = store('interactive-counter').state;
			return (subtotal * vatRate).toFixed(2);
		},
		get total() {
			const { subtotal, vat } = store('interactive-counter').state;
			return (parseFloat(subtotal) + parseFloat(vat)).toFixed(2);
		},
	},
	actions: {
		increment: () => {
			const context = getContext();
			const { products } = store('interactive-counter').state;
			const product = products.find(p => p.id === context.productId);
			if (product) {
				product.quantity = (product.quantity || 0) + 1;
				product.subtotal = (product.price * product.quantity).toFixed(2);
				context.quantity = product.quantity;
				context.subtotal = product.subtotal;
				console.log(`Incremented ${context.productId}:`, { quantity: product.quantity, subtotal: product.subtotal, context });
			} else {
				console.warn('Product not found:', context.productId);
			}
		},
		decrement: () => {
			const context = getContext();
			const { products } = store('interactive-counter').state;
			const product = products.find(p => p.id === context.productId);
			if (product && (product.quantity || 0) > 0) {
				product.quantity -= 1;
				product.subtotal = (product.price * product.quantity).toFixed(2);
				context.quantity = product.quantity;
				context.subtotal = product.subtotal;
				console.log(`Decremented ${context.productId}:`, { quantity: product.quantity, subtotal: product.subtotal, context });
			} else {
				console.warn('Cannot decrement:', context.productId, product?.quantity);
			}
		},
	},
	callbacks: {
		init: () => {
			const { products } = store('interactive-counter').state;
			products.forEach((product, index) => {
				product.quantity = 0;
				product.subtotal = '0.00';
				console.log(`Initialized product ${index}:`, { id: product.id, quantity: product.quantity, subtotal: product.subtotal });
			});
		},
	},
});

Here, you define a store for the interactive-counter namespace, as seen below:

store('interactive-counter', {
	state: { ... },
	actions: { ... },
	callbacks: { ... },
});

Here’s a summary of what these functions and objects do:

  • state: Includes three computed properties (subtotal, vat, total), each returning calculated values according to global state.
  • actions: Contains methods increment and decrement. These update quantities for the selected product, adjust subtotals, and maintain consistency between context and global state.
  • callbacks: Features an init callback that runs on block setup—useful for initialization logic.

The resulting block lets users manage product quantities and see real-time price calculations on the frontend.

An interactive counter built with the Interactivity API
An interactive counter built with the Interactivity API

Why Use the Interactivity API Over Other Methods?

While interactive functionality in WordPress was traditionally achieved through custom JavaScript (often jQuery), or by embedding entire frontend frameworks, these approaches can quickly become cumbersome and hard to maintain. The Interactivity API stands out for several reasons:

  • WordPress-native: Integrated directly with both the block editor and the frontend, this API ensures seamless compatibility and optimal performance with the evolving WordPress ecosystem.
  • Declarative and modular: Rather than managing direct DOM manipulation, you describe how your UI should respond to state, which is easier to reason about and less error-prone.
  • Automatic Asset Loading: JavaScript is only loaded where it’s needed—if your interactive block appears on a page, its logic is loaded, otherwise not, helping improve frontend performance.
  • Server-client synchronization: Directives are rendered server-side along with your block, instantly providing a context-aware, reactive UI when loaded on the frontend.

This makes the Interactivity API both robust and future-proof for projects of all sizes.

Common Use Cases for the Interactivity API

With its wide-ranging capabilities, the Interactivity API is suited for various purposes beyond simple UI toggles:

  • Live Product Listings: Allow users to filter, sort, and interact with product blocks, instantly updating product cards and price displays based on user input.
  • Custom Forms: Validate and preview form content on the fly before submission, or dynamically display follow-up questions according to user responses.
  • Real-time Voting/Polling: Capture votes and update results in real time without a page reload.
  • Interactive Tables or Charts: Filter table data or update graphs/charts interactively, leveraging state management for user personalization.
  • Progressive Content Reveal: Use context and derived state to create step-by-step tutorials, quizzes, and onboarding flows—all natively in Gutenberg.

These use cases showcase the versatility and scalability of the Interactivity API for all kinds of project requirements.

Debugging and Best Practices

Debugging Interactive Blocks

When working with the Interactivity API, effective debugging practices can significantly improve your development speed:

  • Console Logging: Use console.log() in your callbacks and actions to monitor state or debug complex interactions. For example, log changes in derived state or user actions.
  • React DevTools: Since the API uses React under the hood, you can inspect components and state through the React DevTools browser extension for additional insights.
  • Network Monitoring: For blocks that fetch remote data (e.g. REST API calls), leverage your browser’s developer tools to track network requests and responses as user actions trigger updates.

Performance Considerations

To ensure your interactive blocks are as efficient as possible:

  • Keep state minimal: Only store data that absolutely needs to be reactive in state or context. Derived state is optimal for computed values.
  • Careful event handling: Debounce or throttle heavy callbacks if numerous rapid user events could trigger expensive computations or server requests.
  • Modular structure: Break up complex logic into small, reusable functions inside view.js. This keeps your code organized and easy to maintain.

Accessibility Tips

Don’t forget accessibility when building interactive elements! For example:

  • Use ARIA attributes: Bind attributes like aria-expanded or aria-pressed to state via data-wp-bind--aria-* for informative, accessible UIs.
  • Keyboard navigation: Ensure all interactive elements are reachable and operable via keyboard inputs (tab, spacebar, enter, etc.).
  • Appropriate focus management: Set focus where needed after dynamic changes to help screen reader users understand what’s changed.

For more guidance, consult the WordPress block editor accessibility guidelines.

Migration Tips: Converting Existing Blocks to Use the Interactivity API

If you already have static or partially interactive blocks, here’s how to incrementally migrate them:

  1. Identify the interactive areas in your block’s render output (e.g., toggles, counters, editable lists).
  2. Wrap these areas with data-wp-interactive and integrate reactive directives for state, text, and events.
  3. Add or update your view.js file to register a namespace store, initializing global or context state as needed.
  4. Gradually shift existing JavaScript event handlers and logic into actions and callbacks in your Interactivity API store.
  5. Test thoroughly—especially for responsive state updates, accessibility, and frontend consistency.

Adopting the Interactivity API can be incremental, allowing for coexistence with legacy approaches until you’re ready to fully modernize.

Summary

This guide introduced the core features of the WordPress Interactivity API—covering global state, local context, directives, actions, and callbacks. We walked through the process of creating an interactive block from scratch with the @wordpress/create-block-interactive-template, applying these fundamentals in a practical, interactive example.

With these tools and insights, you’re ready to start building engaging, dynamic, and interactive WordPress experiences using the Interactivity API.

Happy coding!

  • Unregistering style variations in a WordPress block theme

    Unregistering style variations in a WordPress block theme

    When you’re creating a custom theme or working with a child theme, there are times when you’ll want to remove or hide particular styling features—whether it’s a single core block or an entire theme style variation. This isn’t always just about personal preference. Removing unused elements can deliver real benefits, including faster performance, more cohesive…

  • Inside a modern WordPress agency tech stack

    Inside a modern WordPress agency tech stack

    Today’s WordPress agencies do so much more than set up plugins or customize themes. These teams are agile and focused, delivering fast, reliable websites, handling complex client demands, and shipping scalable solutions—all while sticking to tight timelines. What enables this efficiency? A carefully chosen, well-integrated tech stack is the critical first step. In this article,…