The WordPress Interactivity API, introduced as stable in WordPress 6.5 (March 2024), is WordPress’s built-in answer to a problem every block developer has hit: how do you add dynamic behaviour — toggle menus, accordion panels, live filters, counters — to a block without shipping a full JavaScript framework or falling back to jQuery?
jQuery solves the problem, but at a cost. A jQuery dependency adds 30–40KB to every page, runs synchronously on the main thread, and directly mutates the DOM in ways that conflict with WordPress’s server-rendered HTML. The result is the “laggy” feeling that tanks Interaction to Next Paint (INP) scores — one of Google’s three Core Web Vitals.
The Interactivity API solves the same problem at ~10KB, shared across every block that uses it on the page, with a declarative model that keeps the server-rendered HTML intact. This guide covers how it works, what the key concepts are, and how to build a real interactive block from scratch.
How the Interactivity API Works
The API is built on two ideas: directives and the Store.
Directives are custom data-wp-* attributes added to your block’s HTML markup. They describe what should happen in response to state changes — “bind this element’s hidden attribute to context.isOpen” — rather than imperatively finding elements and mutating them. WordPress’s runtime reads these attributes on page load and wires up the reactivity layer.
The Store is the JavaScript module where you define your block’s state (data), actions (functions that change state), and derived values (computed from state). When an action changes state, the runtime re-evaluates all directives that reference that state and updates only the DOM nodes that need to change.
This is the key architectural difference from jQuery: instead of writing $('.panel').toggle() (find element, mutate it), you write context.isOpen = !context.isOpen (change state) and let the runtime handle the DOM update. The HTML always reflects the current state — there’s no risk of DOM and state drifting out of sync.
State vs. Context
Before writing code, the most important distinction to understand is global state vs. context:
- Global state (
store().state) is shared across all instances of all blocks using the same namespace. Use it for data that needs to be shared between blocks — a cart item count, a logged-in user name, a modal open/close state controlled from multiple places. - Context (
data-wp-context) is per-block-instance state set in the PHP render template. Each individual block on the page gets its own context. Use it for data that’s local to one block instance — an accordion’s open/closed state, a carousel’s current slide index.
Most interactive blocks use context, not global state. A page with three accordions needs each one to track its own open/closed state independently — that’s context. A page with a cart icon and a mini-cart block that both need to know the cart count — that’s global state.
Directive Reference
The full directive set covers every common interaction pattern:
| Directive | What it does | Example |
|---|---|---|
data-wp-interactive |
Activates the API for a block; sets the namespace | data-wp-interactive="my-plugin" |
data-wp-context |
Sets per-instance context (JSON) | data-wp-context='{"isOpen":false}' |
data-wp-on--click |
Calls an action on click | data-wp-on--click="actions.toggle" |
data-wp-on--mouseover |
Calls an action on mouseover | data-wp-on--mouseover="actions.open" |
data-wp-bind--hidden |
Binds the hidden HTML attribute |
data-wp-bind--hidden="!context.isOpen" |
data-wp-bind--aria-expanded |
Binds any HTML attribute | data-wp-bind--aria-expanded="context.isOpen" |
data-wp-bind--class |
Binds the class attribute to a string | data-wp-bind--class="state.activeClass" |
data-wp-class--open |
Toggles a single CSS class conditionally | data-wp-class--open="context.isOpen" |
data-wp-style--color |
Sets an inline style property | data-wp-style--color="state.textColor" |
data-wp-text |
Sets an element’s text content | data-wp-text="state.counter" |
data-wp-watch |
Runs a callback whenever referenced state changes | data-wp-watch="callbacks.logChange" |
data-wp-init |
Runs a callback once on block initialization | data-wp-init="callbacks.setup" |
data-wp-each |
Renders a list of items from state | data-wp-each="state.items" |
data-wp-key |
Provides a stable key for list items | data-wp-key="item.id" |
Building a Real Block: Toggle Accordion
Here’s a complete, working interactive block — a toggle accordion where clicking the header shows or hides the panel content. It uses four files: block.json, render.php, style.css, and view.js.
block.json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my-plugin/accordion",
"title": "Accordion",
"category": "text",
"attributes": {
"heading": { "type": "string", "default": "Click to expand" },
"content": { "type": "string", "default": "" }
},
"supports": {
"interactivity": true
},
"viewScriptModule": "file:./view.js"
}
Two things to note: "supports": { "interactivity": true } tells WordPress to load the Interactivity API runtime for this block, and viewScriptModule (not viewScript) registers the script as a JavaScript ES module, which is required for the import syntax used in view.js.
render.php
<?php
$context = array( 'isOpen' => false );
?>
<div
<?php echo get_block_wrapper_attributes(); ?>
data-wp-interactive="my-plugin"
<?php echo wp_interactivity_data_wp_context( $context ); ?>
>
<button
data-wp-on--click="actions.toggle"
data-wp-bind--aria-expanded="context.isOpen"
class="accordion-trigger"
>
<?php echo esc_html( $attributes['heading'] ?? 'Click to expand' ); ?>
</button>
<div
class="accordion-panel"
data-wp-bind--hidden="!context.isOpen"
data-wp-class--is-open="context.isOpen"
>
<p><?php echo esc_html( $attributes['content'] ?? '' ); ?></p>
</div>
</div>
wp_interactivity_data_wp_context() is a PHP helper that JSON-encodes the context array and outputs the data-wp-context attribute safely. The rendered HTML contains the initial state (isOpen: false) baked in — the block is fully functional even before JavaScript loads, because the panel is hidden by the initial context value.
view.js
import { store, getContext } from '@wordpress/interactivity';
store( 'my-plugin', {
actions: {
toggle() {
const context = getContext();
context.isOpen = ! context.isOpen;
},
},
} );
getContext() returns the context object for the specific block instance that triggered the action. When a user clicks the button on one accordion, getContext() returns that accordion’s context — not the context of any other accordion on the page. This is how the Interactivity API handles multiple instances of the same block without any additional coordination code.
Compare this to the jQuery equivalent, which would require finding the right sibling panel, toggling a class, managing aria attributes manually, and ensuring the logic doesn’t affect other accordions on the page — 15–20 lines of selector-dependent code vs. 3 lines here.
Server-Side Initial State with wp_interactivity_state()
For global state that needs to be initialised server-side (rather than via context), WordPress provides wp_interactivity_state(). This is useful when your block’s initial state comes from the database or an API call — you compute it in PHP and pass it to the client.
<?php
// In your render.php or plugin's enqueue callback
wp_interactivity_state( 'my-plugin', array(
'cartCount' => WC()->cart->get_cart_contents_count(),
'isLoggedIn' => is_user_logged_in(),
) );
?>
In your view.js, this state is available as state.cartCount and state.isLoggedIn without any additional fetch calls. The data is server-rendered and injected inline — no hydration mismatch, no flash of wrong content.
Interactivity API vs. Alpine.js vs. React vs. jQuery
| Feature | Interactivity API | Alpine.js | React | jQuery |
|---|---|---|---|---|
| Native to WordPress | Yes (6.5+) | No (extra dep) | Yes (editor only) | No (but common) |
| Runtime bundle size | ~10KB (shared) | ~15KB (per-page) | ~45KB (per-page) | ~30KB (per-page) |
| Server-side rendering | Native | Manual | Requires Next.js/SSR | No |
| INP impact | Minimal (deferred) | Low | High (hydration cost) | High (main thread) |
| WordPress block integration | First-class | Manual wiring | Editor side only | Manual wiring |
| Multi-instance state | Context (built-in) | x-data (built-in) | Props/state | Selector scoping |
| Learning curve | Low (if you know HTML) | Low | High | Low (but outdated) |
Alpine.js is the closest comparison — both use HTML-first declarative directives, both are lightweight, and both handle multi-instance state cleanly. The difference is operational: Alpine.js is an extra dependency you own and update; the Interactivity API is part of WordPress core, shared across all blocks, and maintained by the WordPress project. For blocks that ship in plugins or themes, removing an external dependency matters.
React is the right choice for the block editor’s editing experience — WordPress uses React for the editor UI. But for the front end of a block (what visitors see), full React adds 45KB+ of hydration overhead that the Interactivity API’s 10KB replaces. For a detailed breakdown of when React on the front end makes sense vs. when the Interactivity API suffices, the headless WordPress guide covers the architectural trade-offs — the Interactivity API is the better fit for coupled WordPress setups; React shines in headless configurations where Next.js handles the rendering.
Performance and INP Impact
Interaction to Next Paint (INP) measures the time between a user interaction (click, keypress, tap) and the browser’s next visual update. jQuery’s synchronous DOM manipulation on the main thread is the most common cause of poor INP scores on WordPress sites — the click handler runs, mutates the DOM, triggers layout/paint recalculation, all in the same synchronous thread, blocking visual feedback.
The Interactivity API’s actions run as microtasks and update only the DOM nodes that changed (via its reactivity layer), rather than triggering broad reflows. The practical result on INP scores is measurable: sites migrating interactive widgets from jQuery to the Interactivity API typically see INP drop by 40–80ms on mobile devices — enough to move from a “Needs Improvement” (200–500ms) rating to a “Good” (<200ms) rating.
The Lighthouse 100/100 performance guide covers the full INP optimisation stack; the Interactivity API handles the JavaScript interaction layer, which pairs with image optimisation, caching, and script loading strategy to achieve top scores. For the Core Web Vitals context specifically, the Core Web Vitals guide explains how INP replaced FID as a ranking signal and what score thresholds matter.
Integration with Block Themes and FSE
The Interactivity API was designed alongside Full Site Editing — its directives are plain HTML attributes that survive the server render → REST API → block editor round-trip intact. This matters for two reasons:
First, in an FSE block theme, your interactive blocks work in the Site Editor’s preview the same way they work on the front end — no special editor handling required. The directives are just HTML; the editor doesn’t need to interpret them.
Second, in a headless WordPress setup where the front end is served by Next.js, the Interactivity API’s directives are still present in the REST API output. A headless front end that re-implements the same directive logic can use the same PHP-rendered initial state, keeping the server rendering consistent.
The FSE guide covers how block themes and the Site Editor work together; the Interactivity API handles the dynamic behaviour layer within that architecture. For building custom blocks that combine FSE templates with interactive behaviour, the custom Gutenberg block guide covers the full block registration workflow. Block patterns and InnerBlocks covers how to compose multiple interactive blocks into reusable layout patterns.
When to Use the Interactivity API
The Interactivity API is the right choice when:
- You need dynamic behaviour in a front-end block — toggles, tabs, accordions, live counters, filtering
- You’re building for a coupled WordPress setup (not headless)
- INP performance matters (it always does for Core Web Vitals)
- You want WordPress core to own the runtime dependency, not your plugin
It’s not the right choice when:
- You need complex application-level state management across dozens of components (use a full framework)
- You’re building the editor UI (use React — that’s what WordPress uses in the editor)
- You need to support WordPress versions before 6.5 (the API landed stable in 6.5; earlier versions have it as experimental)
For sites that need custom interactive blocks built correctly — reactive, fast, no jQuery, integrated with the block editor — the custom theme development service covers block development using the Interactivity API alongside FSE. If the project involves external API data feeding into block state, the script and style enqueuing guide covers how to load external data safely alongside the Interactivity API’s own asset loading model.


