ES Modules in WordPress 6.5: wp_enqueue_script_module()

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.

<?php
// Enqueue an ES module — WordPress 6.5+
add_action( 'wp_enqueue_scripts', function() {
    // wp_enqueue_script_module() vs wp_enqueue_script():
    // - Always deferred (no 'async' or 'footer' parameters needed)
    // - Loaded with <script type="module"> — 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 <script> tag automatically
// based on what's registered via wp_enqueue_script_module().

const { state, actions } = store( 'myplugin/cart', {
    state: {
        get formattedTotal() {
            return formatPrice( getContext().total );
        },
    },
    actions: {
        updateCount( newCount ) {
            getContext().count = newCount;
        },
    },
} );

// Top-level await works in ES modules (not possible in classic scripts)
const config = await fetch( '/wp-json/myplugin/v1/config' ).then( r => r.json() );
state.taxRate = config.taxRate;

<?php
// Classic script vs Module API comparison:

// Classic (still valid, use when you need IE11 or defer control):
wp_enqueue_script( 'my-script', plugins_url( 'js/main.js', __FILE__ ),
    [ 'jquery' ], '1.0', [ 'strategy' => 'defer' ] );

// Module (preferred for new code using @wordpress/interactivity or modern JS):
wp_enqueue_script_module( '@myplugin/main', plugins_url( 'js/main.js', __FILE__ ),
    [ '@wordpress/interactivity' ] );

// Output an importmap entry for a custom CDN module
add_filter( 'wp_script_module_data', function( array $data ): array {
    $data['imports']['lodash-es'] = 'https://cdn.jsdelivr.net/npm/lodash-es@4/lodash.js';
    return $data;
} );

NOTE: ES modules are supported in all modern browsers. WordPress's import map polyfill handles older browsers via the es-module-shims library. Do not mix wp_enqueue_script() and wp_enqueue_script_module() for the same logical script — they cannot share dependencies through the WordPress dependency injection system.

Leave Comment

Your email address will not be published. Required fields are marked *