WordPress Interactivity API: Building Reactive Islands Without React

WordPress 6.5 stabilised the Interactivity API — a minimal reactive system built into WordPress core that lets you add client-side state, actions, and derived values to server-rendered blocks using data-wp-* directives and a store() function. It is purpose-built for progressive enhancement and works without React, without a build step for the HTML, and with full server-side rendering compatibility.

Problem: Gutenberg blocks with interactive features — a real-time search filter, a paginated carousel, a cart quantity stepper — need client-side state that updates the DOM without a full page reload, but wiring this up with vanilla JavaScript requires reinventing a reactive system.

Solution: Use the WordPress Interactivity API (stable since WordPress 6.5) — declare state with store() in a JavaScript module, bind it to DOM elements with data-wp-bind, data-wp-text, and data-wp-on--click directives, and let the Interactivity API runtime reconcile DOM updates. Server-rendered HTML is hydrated automatically.


The example below builds a filterable tag cloud block: the PHP render callback outputs the markup with data-wp-* directives, and the JavaScript store handles the filter logic — all reactive, with no full-page reload.


 true, 'number' => 50 ] );
if ( empty( $tags ) ) { return; }

// Provide initial server state via wp_interactivity_state()
wp_interactivity_state( 'tag-cloud', [
    'filter'   => '',
    'allTags'  => array_map( fn($t) => [
        'id'    => $t->term_id,
        'name'  => $t->name,
        'count' => $t->count,
        'url'   => get_tag_link( $t->term_id ),
    ], $tags ),
] );
?>

data-wp-interactive="tag-cloud" >


// view.js  (registered as a Script Module)
import { store, getContext, getElement } from '@wordpress/interactivity';

store( 'tag-cloud', {
    actions: {
        setFilter( event ) {
            const { state } = store( 'tag-cloud' );
            state.filter = event.target.value.toLowerCase().trim();
        },
    },
    callbacks: {
        isVisible() {
            const { state }   = store( 'tag-cloud' );
            const { tagName } = getContext();
            return state.filter === '' || tagName.includes( state.filter );
        },
    },
} );


NOTE: wp_interactivity_state() is merged server-side; data passed to it is inlined in the page as a JSON script tag and is publicly visible in the HTML source — never pass nonces, capability flags, or sensitive data through wp_interactivity_state(); use wp_localize_script() with a nonce for any action that requires authentication.