14+ years building on WordPress / Replies in under 5 hours
Development 9 min read · Updated July 2026

The WordPress Interactivity API: A Complete Tutorial with Store, Directives, and Real Code

AK
Ajay Khandal
WordPress Developer
Beyond jQuery: A Complete WordPress Interactivity API Tutorial
TL;DR

The WordPress Interactivity API (stable since WordPress 6.5, March 2024) is WordPress's built-in replacement for jQuery on block front ends. It uses two mechanisms: directives (data-wp-* HTML attributes like data-wp-on--click and data-wp-bind--hidden) that describe what the UI should do, and a Store (defined in view.js using the store() function from @wordpress/interactivity) that holds state and actions. The key distinction: global state (store().state) is shared across all blocks using the same namespace; context (data-wp-context JSON attribute, accessed via getContext() in actions) is per-block-instance state — each accordion on a page gets its own context.isOpen. The runtime is ~10KB and shared across all blocks using it, vs. 30–40KB for jQuery loaded per-page. INP impact: the API's deferred, reactive updates avoid the synchronous main-thread DOM mutations that cause jQuery to hurt Interaction to Next Paint scores. Registration in block.json uses viewScriptModule (not viewScript) to load the view.js as an ES module.

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.

Frequently asked questions

The WordPress Interactivity API is a lightweight, built-in frontend framework for adding dynamic behaviour to WordPress blocks. Introduced as stable in WordPress 6.5 (March 2024), it uses HTML directives (data-wp-* attributes) to declaratively bind block markup to JavaScript state, rather than using jQuery or a full SPA framework. The runtime is ~10KB, shared across every block using it on the page, and it's maintained as part of WordPress core — so there's no additional dependency to manage. It's designed to replace jQuery for block-level interactions (toggles, accordions, counters, live filters) while improving Interaction to Next Paint (INP) scores.

Global state (defined in store().state and accessed as state.propertyName in directives) is shared across every block on the page that uses the same namespace. It's for data multiple blocks need to share — a cart count, a logged-in status, a globally-controlled modal. Context (set via data-wp-context JSON attribute in render.php, accessed in actions via getContext()) is per-block-instance: each individual block on the page gets its own isolated context object. It's for data that belongs to one block instance — whether this specific accordion is open, which slide this specific carousel is showing. Most interactive blocks use context, not global state, because block instances need to be independent.

No. The Interactivity API replaces jQuery for block front-end interactions in WordPress 6.5+. jQuery adds 30–40KB per page, runs on the main thread, and directly mutates the DOM in ways that hurt Interaction to Next Paint (INP) scores. The Interactivity API's ~10KB shared runtime handles the same toggle/tab/accordion/counter interactions declaratively, without main-thread blocking. For plugins or themes that still load jQuery for legacy reasons, those should be refactored over time — but for new blocks built on WordPress 6.5+, jQuery is not needed for block-level interactivity.

data-wp-interactive is the directive that activates the Interactivity API for a block and sets its namespace. It's added to the outermost element of the block in render.php. The value is the namespace string matching the first argument of your store() call in view.js — for example, data-wp-interactive="my-plugin" pairs with store('my-plugin', {...}). Without this directive, none of the other data-wp-* directives inside the block will be processed by the runtime. Every interactive block must have exactly one data-wp-interactive on its wrapper element.

The Interactivity API improves Interaction to Next Paint (INP), one of Google's three Core Web Vitals ranking signals. INP measures the time between a user input (click, keypress, tap) and the browser's next visual update. jQuery's synchronous DOM manipulation runs on the main thread and can delay this update by 100–300ms on mobile. The Interactivity API's reactive updates are deferred and target only the DOM nodes that changed, not the entire page. Sites migrating interactive widgets from jQuery to the Interactivity API typically see INP improve by 40–80ms — enough to shift from the Needs Improvement range (200–500ms) to the Good range (under 200ms). INP replaced First Input Delay (FID) as a Core Web Vitals metric in March 2024, making this improvement directly relevant to search rankings.

viewScriptModule registers your view.js file as a JavaScript ES module, which enables the import statement used in view.js (import { store, getContext } from '@wordpress/interactivity'). viewScript (without 'Module') registers a classic script using wp_enqueue_script(), which doesn't support ES module import/export syntax and would cause a syntax error when the Interactivity API's import is encountered. For any block using the Interactivity API, use viewScriptModule. WordPress 6.5+ handles the module loading automatically — you don't need to add type="module" manually. If your block doesn't need ES module imports (e.g. it uses a classic UMD bundle), viewScript is still valid, but the Interactivity API requires viewScriptModule.

AK

Written by Ajay Khandal

WordPress Developer — building, fixing and speeding up WordPress sites.

Work with me →