How to enqueue scripts with type=module attribute via filters in WordPress

Banner Siteground

Introduction

This article documents, in exhaustive detail, how to enqueue JavaScript files in WordPress and ensure the generated ltscriptgt tag has type=module using filters. It covers the why, browser behavior, multiple approaches, robust sample implementations, nomodule fallbacks, inline module script strategies, import maps, SRI/crossorigin handling, compatibility notes and debugging tips. All examples are ready-to-copy PHP code snippets suitable for a theme or plugin.

Why use type=module?

ES modules provide scoped imports, top-level await, and are strictly in strict mode. Marking a ltscriptgt with type=module tells modern browsers to parse it as an ES module, enabling import/export, deferred execution semantics, and other module-specific behavior. For modern build pipelines and clean dependency graphs, modules are preferable for client-side applications.

Important module behavior to understand

  • Modules are deferred by default: A script with type=module is loaded and executed after the document has been parsed (similar to defer), so you should avoid also setting defer manually.
  • Scope: Module scripts execute in their own scope, not on the global window by default (exported symbols must be explicitly assigned to window if you need them globally).
  • Same-origin CORS: If a module is loaded from another origin, the server must respond with proper CORS headers (or use crossorigin attribute). Failure may result in a CORS error.
  • Nomodule fallback: Older browsers that do not support modules will ignore type=module scripts. To support them you must provide a separate nomodule script or runtime-transpiled bundle.

WordPress and type attributes

WordPress core API for registering and enqueuing scripts (wp_register_script, wp_enqueue_script) does not provide an explicit parameter in the enqueue call to set type=module on the resulting ltscriptgt tag. The recommended and reliable approach is to use the script_loader_tag filter to adjust the final HTML tag the platform prints for a given handle. Below are multiple patterns and full examples using that filter.

Basic pattern: mark a single handle as a module

This is the simplest approach. Register or enqueue your script normally, then use script_loader_tag to insert type=module for the specific handle.

lt?php
add_action( wp_enqueue_scripts, mytheme_enqueue_scripts )
function mytheme_enqueue_scripts() {
    // Register or enqueue as usual. Last parameter is in_footer (true recommended).
    wp_enqueue_script(
        my-module-script,
        get_template_directory_uri() . /js/main.js,
        array(),
        1.0.0,
        true
    )
}

/
  Add type=module to the script tag for the handle my-module-script.
 
  @param string tag    The original HTML script tag.
  @param string handle The registered script handle.
  @param string src    The script src URL.
  @return string Modified script tag.
 /
add_filter( script_loader_tag, mytheme_add_type_module, 10, 3 )
function mytheme_add_type_module( tag, handle, src ) {
    if ( my-module-script !== handle ) {
        return tag
    }

    // If the tag already contains a type attribute, leave it alone.
    if ( preg_match( /btypes=/i, tag ) ) {
        return tag
    }

    // Insert type=module right after the opening