How to use import maps or alternatives in WordPress in WordPress

Contents

Overview: import maps and the problem they solve

Import maps are a browser feature that let you map bare ES module specifiers (for example react or @my/lib) to full URLs so that client-side ES module imports can be written simply and resolved by the browser without a bundler. For modern front-end workflows this can reduce or eliminate the need for an application bundler when using native ES modules.

WordPress themes and plugins typically use PHP-driven script registration and the classic global script loader, so integrating import maps requires emitting the correct ltscript type=importmapgt and module script tags in the correct order and handling older browsers that do not support import maps. This article explains how import maps work, browser support and polyfills, and provides concrete WordPress-ready patterns (theme and plugin) plus robust alternatives using modern build tooling (Vite/webpack/esbuild/@wordpress/scripts) and fallbacks for legacy browsers.

Browser support, shims and when to use them

As of this writing, ES modules are well supported in modern browsers, but native import map support is not universal. Two practical routes:

  • Native import maps: use when you target modern browsers that support import maps.
  • es-module-shims (polyfill): a popular shim that implements import map behavior and supports browsers without native import map support. Use with type=importmap-shim and type=module-shim for scripts.

When you need the broadest compatibility, build a dual strategy: use import maps module scripts for modern browsers and provide a nomodule fallback (classic bundled script) for legacy browsers.

Core concepts in import maps

  • Import map JSON: a small JSON object placed inside a ltscript type=importmapgt element mapping specifiers to URLs. Example keys: imports and scopes.
  • Specifiers: the bare module names used in your JS import statements (for example import React from react or import App from @theme/app).
  • Ordering: the import map script must appear in the document before any module scripts that rely on it.
  • CSP: import map scripts are treated as scripts for CSP purposes. If your site disallows inline scripts, you must provide a nonce or a hash in CSP, or place the import map in an allowed external file loaded with an approved URL.

Minimal WordPress theme example (native import map module script)

This example shows a small theme-level approach: emit an import map in the document head and register a module script for your front-end app. The import map must be printed before the module script using the wp_head action to print the import map and wp_enqueue_script plus a filter to set type=module on the script tag ensures correct ordering.

functions.php (theme):

lt?php
// 1) Print import map in ltheadgt
add_action( wp_head, function() {
    // Map @theme/app to the URL of the module file built or shipped in the theme
    import_map = array(
        imports =gt array(
            @theme/app =gt get_theme_file_uri( /assets/js/app.module.js ),
            lodash =gt https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.js,
        )
    )
    echo ltltscript type=importmapgt . PHP_EOL
    echo wp_json_encode( import_map, JSON_UNESCAPED_SLASHES  JSON_PRETTY_PRINT ) . PHP_EOL
    echo lt/scriptgt . PHP_EOL
} )
 
// 2) Register and enqueue the module script well use a filter to inject type=module
add_action( wp_enqueue_scripts, function() {
    // Register script with WordPress so it prints a proper src and version
    wp_register_script(
        theme-app-module,
        get_theme_file_uri( /assets/js/loader.js ), // loader.js is a tiny module that imports @theme/app
        array(), // dependencies are not needed here loader is a module
        filemtime( get_theme_file_path( /assets/js/loader.js ) )
    )
    wp_enqueue_script( theme-app-module )
} )

// 3) Add type=module to the script tag for our handle
add_filter( script_loader_tag, function( tag, handle, src ) {
    if ( theme-app-module === handle ) {
        // Replace the classic script tag with a module script tag
        tag = sprintf( , esc_url( src ) )
    }
    return tag
}, 10, 3 )
?gt

Example import map JSON (the content printed by the PHP above). This is for demonstration — the PHP printed JSON for you if you need a static example here it looks like:

{
  imports: {
    @theme/app: https://example.com/wp-content/themes/mytheme/assets/js/app.module.js,
    lodash: https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.js
  }
}

Example loader.js (a tiny module that imports the app via the bare specifier):

// loader.js
import App from @theme/app

document.addEventListener(DOMContentLoaded, () =gt {
  App.init  App.init()
})

Using es-module-shims to add import map support on older browsers

For broader browser support for import maps, include es-module-shims. With the shim you use type=importmap-shim and type=module-shim. The shim itself must be loaded before the import map script tag it can be enqueued as a normal script (classic script) because it is not a module. The shim will process the import map and make it available to module-shim scripts.

Example theme/plugin code to add es-module-shims plus importmap-shim and a module-shim loader:

lt?php
add_action( wp_head, function() {
    // 1) Include es-module-shims from a CDN (or serve a local copy)
    echo ltscript src=https://ga.jspm.io/npm:es-module-shims@1.6.3/dist/es-module-shims.min.jsgtlt/scriptgt . PHP_EOL
 
    // 2) Print an import map using importmap-shim so shim can process it
    import_map = array(
        imports =gt array(
            @theme/app =gt get_theme_file_uri( /assets/js/app.module.js ),
        )
    )
    echo ltscript type=importmap-shimgt . PHP_EOL
    echo wp_json_encode( import_map, JSON_UNESCAPED_SLASHES  JSON_PRETTY_PRINT ) . PHP_EOL
    echo lt/scriptgt . PHP_EOL
 
    // 3) Print a module-shim loader that uses those imports
    echo ltscript type=module-shim src= . esc_url( get_theme_file_uri( /assets/js/loader.js ) ) . gtlt/scriptgt . PHP_EOL
} )
?gt

Notes about the shim approach

  • The shim is slightly larger and slower than native import maps but enables consistent behavior everywhere.
  • You can host the shim yourself (best for production) to avoid third-party CDN availability concerns.
  • es-module-shims also supports preloading via rel=modulepreload patterns used by build tools.

Nomodule fallback for legacy browsers (production-ready pattern)

To support older browsers that neither support ES modules nor import maps, produce a legacy bundle (classic build) and load it with the nomodule attribute. Modern browsers ignore nomodule scripts older browsers run them. This yields a practical, production-grade approach: module (or module-shim) for modern browsers nomodule for legacy.

Example of printing both module and nomodule script tags (PHP snippet):

lt?php
// Enqueue modern module loader (uses import maps or module files)
wp_register_script( theme-app-module, get_theme_file_uri( /dist/app.module.js ), array(), filemtime( get_theme_file_path( /dist/app.module.js ) ) )
wp_enqueue_script( theme-app-module )

// Enqueue legacy bundle (nomodule)
wp_register_script( theme-app-legacy, get_theme_file_uri( /dist/app.legacy.js ), array(), filemtime( get_theme_file_path( /dist/app.legacy.js ) ) )
wp_enqueue_script( theme-app-legacy )

// Filter to add attributes:
add_filter( script_loader_tag, function( tag, handle, src ) {
    if ( theme-app-module === handle ) {
        return sprintf( , esc_url( src ) )
    }
    if ( theme-app-legacy === handle ) {
        return sprintf( , esc_url( src ) )
    }
    return tag
}, 10, 3 )
?gt

Alternatives to import maps: bundlers and tooling recommended for production

Import maps are great for small-to-medium apps or where you want simple dependency composition without a heavy build step. For larger applications, code-splitting, tree-shaking, asset transforms, and aggressive optimizations, typical bundlers give better control. Recommended options for WordPress projects:

  • Vite — extremely fast dev server and modern build outputs ES module and legacy bundles integrates well with WordPress by building assets into your theme/plugin and enqueuing them with file-based versioning (filemtime).
  • esbuild — super-fast bundler/transformer. Use it for quick builds or as part of a larger pipeline.
  • webpack — mature ecosystem, good for complex needs with many loaders and plugins.
  • @wordpress/scripts — WordPress-focused bundling configuration built on webpack and Babel for block and plugin authors integrates with WordPress build expectations.

How to enqueue built files produced by a bundler

The common pattern: build assets to a dist directory, then use PHP to register/enqueue those files and use filemtime (or a hashed manifest) for cache-busting. If the build produces separate modern and legacy bundles, enqueue both and use the script_loader_tag filter to add the proper attributes (type=module and nomodule).

Example: enqueuing Vite-built assets (simple pattern)

lt?php
function theme_enqueue_bundled_assets() {
    dist = get_theme_file_path( /dist )
    uri  = get_theme_file_uri( /dist )

    // Modern module build
    modern_js = /dist/app.module.js
    wp_register_script( theme-app-modern, uri . /app.module.js, array(), filemtime( dist . /app.module.js ) )
    wp_enqueue_script( theme-app-modern )

    // Legacy build (if produced)
    if ( file_exists( dist . /app.legacy.js ) ) {
        wp_register_script( theme-app-legacy, uri . /app.legacy.js, array(), filemtime( dist . /app.legacy.js ) )
        wp_enqueue_script( theme-app-legacy )
    }
}
add_action( wp_enqueue_scripts, theme_enqueue_bundled_assets )

// Add attributes for module/nomodule
add_filter( script_loader_tag, function( tag, handle, src ) {
    if ( theme-app-modern === handle ) {
        return sprintf( , esc_url( src ) )
    }
    if ( theme-app-legacy === handle ) {
        return sprintf( , esc_url( src ) )
    }
    return tag
}, 10, 3 )
?gt

WordPress block editor (Gutenberg) considerations

  • If you build block editor scripts as ES modules, they must be compatible with WordPress editor loading model. Gutenberg assumes classic script tags for many WP scripts, so test carefully when switching to module-based delivery.
  • For block editor code, prefer building with @wordpress/scripts or an established block build pipeline, then enqueue the produced bundle as either a module or a classic script depending on your compatibility needs.
  • If you want to use import maps for editor assets, ensure that the import map and module scripts are enqueued in the editor context using hooks like enqueue_block_editor_assets.

Content Security Policy (CSP) and import maps

Import map tags are treated as scripts by browsers. If your site has a restrictive CSP that blocks inline scripts, a raw inline ltscript type=importmapgt may be blocked unless you provide a matching nonce or hash in your CSP header. Two options:

  1. Emit a nonce attribute on the import map script and include the same nonce in the CSP headers script-src via a policy generation mechanism. You must also add the nonce to any inline module-shim or inline scripts that need it.
  2. Host the import map JSON externally and reference it as a static file you can allow with a CSP source (for example by hosting on your own domain and including that origin in script-src). This loses some dynamic flexibility but avoids inline script restrictions.

Practical tips, pitfalls and recommendations

  • Order matters: import map must be before any module that depends on it.
  • Development vs production: use import maps and native modules to accelerate development iterations. For production, consider bundling/minifying for performance and for supporting older browsers.
  • Cache-busting: use filemtime() or hashed filenames from your build to avoid stale caches when deploying updates.
  • WordPress script concatenation/minification: some plugins or host-level optimizers may alter or combine script tags. When using import maps or module/nomodule strategies, disable automatic concatenation/minification for scripts that require specific attributes or ordering.
  • Testing: test in multiple browser families (Chromium, Firefox, Safari, Edge) and test legacy fallbacks with older Safari or IE11 (IE11 will only run nomodule bundles or classic scripts).
  • Security: treat import maps and module scripts as code. Keep external CDN integrity in mind add SRI or host critical libraries yourself for tighter security.

Troubleshooting checklist

  1. If modules fail to resolve, check that the import map was printed and reachable in the page source before the module script.
  2. Check the browser console for errors that indicate unsupported import map syntax or failed network requests to resolved URLs.
  3. If a script is being concatenated or altered by a plugin, exclude it from that plugin or set the plugin to preserve original tags for those handles.
  4. For CSP issues, inspect the network and console for CSP violation reports and adjust the policy or add nonces/hashes as needed.

Summary and recommended patterns

For modern WordPress projects:

  • Use import maps for simple modular setups or during development to leverage browser-native ES modules and easily map specifiers to CDNs or internal files.
  • Use es-module-shims if you want import map semantics broadly across browsers without building separate legacy bundles.
  • For production-grade performance and broad compatibility, build modern (module) and legacy (nomodule) bundles with Vite/webpack/esbuild. Enqueue both and add type=module and nomodule attributes via a script_loader_tag filter.
  • Always ensure the import map or shim is printed before modules and watch out for minifiers/optimizers that reorder or combine script tags.

References and useful links



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

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