# WordPress Interactivity API for storefront UI

Replace jQuery widgets with the Interactivity API. Directives, store, SEO-safe server HTML and ~10KB runtime.

- Date: 2026-08-26
- Category: WordPress

The WordPress Interactivity API is how I now ship storefront UI that used to live in jQuery widgets: quantity steppers, filter drawers, gallery toggles, and sticky add-to-cart bars. I am a Gold Coast web developer with 18 years on business sites and shops. In 2026 this API is no longer a preview. Core already uses it, custom blocks can use it, and the runtime is small enough that I can replace a pile of plugin scripts without turning the product page into a single-page app.

This note is for people who still maintain WooCommerce or content-heavy WordPress stores and keep adding "just one more" accordion plugin. It is also for anyone building a [WordPress block theme](https://alanvo.com/blog/wordpress-block-themes/) who needs clicks to work without hydration mismatches.

## Why the WordPress Interactivity API matters in 2026

The WordPress Interactivity API matters because storefront widgets still need to update the DOM after first paint, and the old stack for that job is too heavy. jQuery UI, custom `$(document).on` handlers, and three "lite" accordion plugins all fight for the same nodes. Each one ships its own CSS, its own event bus, and its own copy of "toggle this class". Field INP on product and cart pages is where that fight shows up.

WordPress 6.5 shipped the API in core. By WordPress 7.0 the store gained a `watch()` helper for side effects that are not tied to a single element, and the `core/router` store now seeds `state.url` on the server. I do not treat those as trivia. They mean a gallery block, a mini-cart, and a filter chip can share one mental model: server HTML that is already correct, plus a ~10KB runtime that binds directives to a store.

I still see sites where the "interactive" layer is a React island for a size chart, a Vue widget for reviews, and jQuery for the rest. That mix is how you get a 200ms click on Add to cart and a 900ms click on the quantity plus button. The API does not magically make payment gateways faster. It does let me keep the HTML that search engines and screen readers already see, then attach behaviour without a second render tree.

If you are comparing editors, this is the other half of the [Gutenberg vs Elementor performance](https://alanvo.com/blog/gutenberg-vs-elementor-performance/) argument. Blocks can be slow when they dump inline CSS. They can also be fast when interactivity stays in directives instead of a page-builder widget pack.

![WordPress Interactivity API in the block editor on a laptop](../../images/blog/wordpress-interactivity-api-1.jpg)

## How the WordPress Interactivity API actually works

The WordPress Interactivity API actually works as HTML attributes plus a namespaced store, not as a virtual DOM you hydrate. You mark a subtree with `data-wp-interactive="yourNamespace"`. Directives on that subtree read and write state. The PHP render (or the saved block HTML) already contains the open, closed, selected, and disabled markup that a no-JS visitor should see. The runtime then keeps that markup in sync when state changes.

Three pieces sit together:

1. Directives. Attributes such as `data-wp-on--click`, `data-wp-bind--aria-expanded`, `data-wp-class--is-open`, `data-wp-text`, and `data-wp-context`. They are the public contract. If a junior can read the HTML and guess what happens on click, you wrote it well.
2. Store. A `store('yourNamespace', { state, actions, callbacks })` object in `view.js`, imported from `@wordpress/interactivity`. State is a reactive proxy. Actions are the functions directives call. Callbacks (and `data-wp-watch` / `data-wp-init`) run when values change.
3. Server state. `wp_interactivity_state()` in PHP seeds prices, stock, and initial open/closed flags so the first HTML is not an empty shell. `wp_interactivity_config()` holds non-reactive config. `wp_interactivity_data_wp_context()` prints a JSON context blob onto a node.

The official [Interactivity API reference](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/) is the source of truth for directive names and store helpers. I keep that tab open when I name actions. Guessing a directive from memory is how you ship `data-wp-on-click` (wrong) instead of `data-wp-on--click` (right).

On a furniture PDP I care about three interactions: gallery thumbs, a materials accordion, and a sticky bar that appears after the hero. Each used to be a separate plugin. Each can now be one block or a small cluster of blocks that share a namespace. The HTML still has real `img` tags and a real button. Googlebot does not need to execute the store to index the product name.

## Directives, store, and server HTML

Directives only run inside an activated subtree. If you forget `data-wp-interactive`, nothing binds and you will spend an hour blaming `viewScriptModule`. Put the namespace on the wrapper that owns the UI, not on every inner span.

Context is local. `data-wp-context='{ "isOpen": false }'` on an accordion item means each item can open on its own. Global store state is for values every instance should share: currency, cart count, a feature flag. Mixing those up is the first production bug I look for. An accordion that closes every other accordion on the page usually wrote `isOpen` into the store instead of context.

![Developer desk with two monitors during WordPress Interactivity API work](../../images/blog/wordpress-interactivity-api-2.jpg)

Server HTML is the SEO contract. If the filter drawer is closed for most visitors, render it closed. Do not render an empty `<div>` and let JS paint 40 colour swatches after load. The API is built so the closed drawer can still exist in the DOM with `hidden` bound to state. Crawlers and no-JS users get the same product grid you intended.

I use `wp_interactivity_state()` for anything the theme already knows in PHP: variation prices, whether the SKU is in stock, the initial slide index from a query param. I do not put secrets there. Anything in state is in the page.

Private stores exist when two blocks should not collide on a public namespace. If you are shipping a plugin other people will install next to yours, private is the default. On a one-site theme I still namespace with a prefix I own, not `store` or `app`.

WordPress 7.0 `watch()` is for analytics and logging that should not hang off a DOM node. A client-side navigation in a query-loop style UI can call a page-view helper when `state.url` changes. I still fire purchase events from a proper dataLayer on the thank-you page. Do not invent a second ecommerce tracker inside a gallery store.

## Production checklist for storefront blocks

A production storefront block is not "it toggles in my local editor". Work through this list before it hits a catalogue.

1. Confirm the block uses a `view.js` (or view script module) that imports `store` from `@wordpress/interactivity`. Editor-only JS does not run for customers.
2. Put `data-wp-interactive` on the wrapper. Use a stable namespace string you will not rename in a year.
3. Render complete HTML in `render.php` (dynamic block) or in saved markup that is valid without JS. Buttons need real `<button>` elements, not clickable divs.
4. Seed `wp_interactivity_state()` with the values PHP already has. Do not fetch the product object again in JS on first paint if PHP already printed it.
5. Bind ARIA as you bind classes. `aria-expanded`, `aria-hidden`, and `aria-controls` should follow state, not a jQuery `slideToggle` that forgets them.
6. Keep actions small. `toggleOpen`, `selectSlide`, `incrementQty` beat a 80-line `handleEverything`.
7. Do not register a second copy of the store under a slightly different namespace. Merge is by namespace. Typos create silent no-ops.
8. Test with the Interactivity runtime only. Disable the old widget plugin on staging, not "later".
9. Test keyboard: Enter and Space on the control, Escape on a drawer, focus return when the drawer closes.
10. Test a second instance of the same block on one page. Mini-cart plus sticky bar will share state whether you meant them to or not.
11. Check the script size in the network panel. You are aiming for the small core runtime plus your view file, not an extra copy of React.
12. Read the [block editor handbook](https://developer.wordpress.org/block-editor/) when the block also has an editor UI. Editor and view are different scripts. Do not put store actions in `index.js` and assume the front end has them.

![Website wireframes on paper next to a keyboard](../../images/blog/wordpress-interactivity-api-3.jpg)

I keep a staging URL with query-string flags to force empty cart, out-of-stock, and a long product title. Interactivity bugs hide in the boring states.

## What breaks when you mix it with jQuery

Mixing the API with leftover jQuery is the failure mode I see most on upgrades. The store sets `class="is-open"` and a 2016 theme script immediately removes it because it only understands `.accordion-active`. You get a flash, then a dead control.

Other breaks:

- Duplicate event listeners. A plugin still binds `click` on `.qty` while `data-wp-on--click` also fires. Quantity jumps by two.
- HTML comments and Woo templates. If a plugin wraps your block in an extra div, context can sit on the wrong ancestor. `getContext()` then returns unexpected keys.
- Full-page cache. State seeded for user A (logged-in price) must not ship to user B. Interactive blocks do not exempt you from cache keys. Vary on currency, login, and localisation the same way you already should.
- Optimistic UI without a server check. A quantity stepper that never talks to Woo session state will lie at checkout. Use the API for the control, still post through Woo's cart endpoints.
- `data-wp-each` lists that PHP also printed. You can double the list if you both loop in Twig-style PHP and loop in the directive.
- Namespace collisions between a theme and a plugin that both used `woocommerce`.
- Editor preview vs front. The editor canvas is not a reliable INP test. Always click the front-end page.

When a control "does nothing", I view source and search for `data-wp-interactive`. If it is missing, the runtime never started. If it is present and `view.js` 404s because of a build path, the runtime started with no actions.

## How to measure interactivity work

Measure field INP on the templates that contain the widgets, not a Lighthouse pass on the homepage. Chrome User Experience Report and GA4 (if you still send web-vitals) should be sliced by page type: PDP, PLP, cart. Lab tests on a throttled cable profile are for before/after of a single control.

![Content editor reviewing a page on a tablet](../../images/blog/wordpress-interactivity-api-4.jpg)

I record:

- Interaction to next paint for the exact click: quantity plus, filter chip, gallery thumb, accordion header.
- Total JS from `/wp-content` on that template, minus analytics. You want the accordion plugin bytes gone.
- Time to first click that changes visible state, from a cold load with cache primed as a customer would see it.
- Error rate: console exceptions after the store loads. A thrown action kills later clicks in that subtree more often than people expect.

Do not claim a conversion lift from a widget rewrite unless you already have that number on a work case. I will not invent one here. The honest win is fewer competing scripts and clicks that feel like the page is listening.

Compare a staging branch with the old jQuery widget against the directive version using the same product URL, same image set, same third-party tags. If you also deferred tags, isolate that change. Otherwise you will credit the API for a GTM trim.

## Related work on this site

Storefront UI is where this API pays rent. The [Skanvi furniture storefront](https://alanvo.com/work/skanvi/) is a WooCommerce catalogue whose product story and photography cannot afford a sluggish widget pack on the PDP. The [BridgeWorx ticketing site](https://alanvo.com/work/bridgeworx/) is WordPress and WooCommerce for events, where drawers, filters, and add-to-cart style actions happen under time pressure when a show is about to sell. Both were contract development on an agency team. The same interaction rules apply whether the SKU is a sofa or a ticket: server HTML first, small runtime, no leftover jQuery on the same node.

## FAQ

### What is the WordPress Interactivity API for if I already know React?

The WordPress Interactivity API is for front-end blocks that must stay SEO-safe HTML, not for rebuilding wp-admin. React still powers the editor. On the customer-facing page you want directives and a small runtime so you do not hydrate a tree Google already could have read as static markup.

### Does the WordPress Interactivity API replace WooCommerce fragments?

The WordPress Interactivity API does not replace cart fragments, checkout updates, or payment SDK behaviour by itself. It replaces the local widgets around those flows: steppers, drawers, tabs, and galleries. Cart totals still need Woo's session and notices. If a fragment refresh wipes a node the store owns, you must re-bind or keep the interactive root outside the fragment target.

![Laptop and coffee while shipping WordPress Interactivity API](../../images/blog/wordpress-interactivity-api-5.jpg)

### Can I use the WordPress Interactivity API inside a classic theme?

You can use the WordPress Interactivity API in a classic theme if the block is registered and the view script enqueues on the front. You do not need a block theme for a single interactive block. You do need a block that actually prints the directives. Dropping attributes into a random PHP template without enqueueing the runtime will do nothing.

### Is the WordPress Interactivity API worth it on a five-page brochure site?

The WordPress Interactivity API is rarely worth a rewrite on a five-page brochure that only needs a mobile nav you can do with a details element. It is worth it when you keep installing plugins to toggle, filter, and step through catalogue UI. Start with the noisiest widget on the PDP, not with a platform migration story.



HTML version: https://alanvo.com/blog/wordpress-interactivity-api/
