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
