WordPress 6.5 introduced wp_enqueue_script_module(), the official API for loading JavaScript ES modules natively in the browser — no bundler required for simple scripts. ES modules use import/export, are deferred by default, and run in strict mode automatically.
Problem: WordPress themes and plugins traditionally load JavaScript as classic scripts with wp_enqueue_script() — using native ES module import/export syntax requires a bundler or breaks browser compatibility.
Solution: Use wp_enqueue_script_module() (introduced in WordPress 6.5) to register native ES modules. WordPress generates an import map automatically for known module identifiers, adds type="module" to the script tag, and handles dependencies declared in the module's imports array — no bundler required for simple module graphs.
The examples below enqueue a script module for a Gutenberg block front end, import from another module, and show the key differences between the classic wp_enqueue_script() and the new module API.
— runs in strict mode
// - Uses import maps for dependency resolution
// - Handles circular imports correctly
// - Each module runs once even if enqueued multiple times
wp_enqueue_script_module(
'@myplugin/cart-counter', // module identifier (handle)
plugins_url( 'js/cart-counter.js', __FILE__ ),
[ '@wordpress/interactivity' ], // module dependencies (handles)
'1.0.0'
);
// Register without enqueueing (lazy-loaded later)
wp_register_script_module(
'@myplugin/analytics',
plugins_url( 'js/analytics.js', __FILE__ )
);
} );
// In block.json, use 'viewScriptModule' (not 'viewScript') for ES modules:
// {
// "viewScriptModule": "file:./view.js"
// }
// WordPress auto-generates the module handle: "myplugin/my-block//view"
The module file itself uses native ES module syntax:
// js/cart-counter.js — runs as a native ES module
// Import from another WordPress module via its handle
import { store, getContext } from '@wordpress/interactivity';
// Import from a local module — relative paths work normally
import { formatPrice } from './utils/format.js';
// Import maps handle bare specifiers like '@wordpress/interactivity'
// WordPress 6.5+ outputs an importmap