HTML import maps: give browser modules a name they can resolve

If a browser reports that it cannot resolve the module specifier workshop, put a mapping for that name in a <script type="importmap"> before the module that imports it. The map tells the browser which URL the name means. It does not download a package or turn a CommonJS package into a browser module. HTML Standard: import maps

For a small equipment page, you might want the page's code to import workshop while keeping the implementation in a versioned folder. Changing one map entry can then point that name at the intended module.

Put the map before the importing module

Use this illustrative folder layout on an HTTP development server:

equipment/
  index.html
  modules/
    workshop-v1.js

The complete index.html is:

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Equipment return</title>
<p id="notice">Loading return instructions...</p>
<script type="importmap">
{
  "imports": {
    "workshop": "./modules/workshop-v1.js"
  }
}
</script>
<script type="module">
  import { returnNotice } from "workshop";
  document.querySelector("#notice").textContent = returnNotice;
</script>
</html>

Save this as modules/workshop-v1.js:

export const returnNotice = "Return borrowed tripods to the media desk by Friday.";

The expected visible result is the Friday instruction. workshop is a bare specifier: it carries no ./, ../, or absolute URL. The map supplies the missing address. Without the map, the browser cannot infer that it means a file in modules.

The JSON uses double quotes and has no comments or trailing comma. Keep it inline; this import-map form does not load a map through a src attribute. Import-map element requirements

Match one file or a directory prefix

An exact mapping for workshop does not also map workshop/checklist.js. To map a family of module paths, put a trailing slash on both sides:

{
  "imports": {
    "workshop/": "./modules/workshop-v1/"
  }
}

Now import "workshop/checklist.js" resolves to ./modules/workshop-v1/checklist.js. That file still has to exist and be a usable JavaScript module. A missing file remains a network error after name resolution succeeds.

Relative destination URLs resolve against the import map's base URL. Moving index.html into another folder can therefore change the destination. Inspect the requested URL in the Network panel when the map looks correct but the file returns 404.

Use scopes when the importing code needs a different version

The top-level imports object supplies the normal mapping. A scopes entry can override a name for modules imported from a particular URL prefix. The deciding address belongs to the module doing the importing, rather than a visible section of the page.

{
  "imports": { "workshop": "./modules/workshop-v1.js" },
  "scopes": {
    "./legacy/": { "workshop": "./modules/workshop-legacy.js" }
  }
}

A module under ./legacy/ can now resolve workshop differently from the page's main module. Keep a single early map when that meets your needs. Adding a later map cannot reliably replace names that have already been resolved.

Separate resolution errors from loading errors

If the console still reports an unresolved bare specifier, compare the import string with the map key and check map order and JSON syntax. If the request reaches the intended URL but fails, check the response status, JavaScript content type, and cross-origin access instead.

Serve the fixture over HTTP during development so local-file restrictions do not obscure the result. Why JavaScript fails in an HTML page covers those environment checks; hosting a JavaScript file covers paths and caching after deployment.

Finish with the requested module URL and the displayed return instruction agreeing. Keep the mapped module files with the page when moving or handing off the project.

Keep reading