JavaScript Import Maps: Resolving Bare Specifiers
Import maps are a native web standard that enables browsers to map
module specifiers to specific URLs or paths, allowing developers to use
bare module specifiers directly in client-side JavaScript without a
bundler. This article covers what import maps are, the problem of bare
specifiers in native ES modules, how to implement import maps using the
<script type="importmap"> tag, and the practical
benefits they bring to web development.
The Bare Module Specifier Problem
In environments like Node.js or bundler-driven setups (e.g., Vite, Webpack), JavaScript developers regularly use bare module specifiers to import dependencies:
import React from 'react';
import { debounce } from 'lodash';When modern browsers natively introduced ECMAScript Modules (ESM) via
<script type="module">, they enforced a strict rule:
module specifiers must be absolute URLs, relative URLs starting with
./, ../, or root-relative paths starting with
/. A bare specifier like 'react' does not
resolve to a file path natively, causing the browser to throw an
error:
TypeError: Failed to resolve module specifier "react". Relative references must start with either "/", "./", or "../".
Historically, resolving this required running a build step to rewrite imports into direct file paths or bundle them into a single file.
What Are Import Maps?
An import map is a JSON structure embedded directly in an HTML document that instructs the browser on how to resolve module specifiers. By defining an import map, you can associate bare specifiers or custom aliases with absolute or relative URLs, allowing native ES modules to consume external libraries cleanly.
Import maps are defined using a <script> tag with
the type="importmap" attribute:
<script type="importmap">
{
"imports": {
"lodash": "https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/lodash.js",
"utils/": "/src/utils/"
}
}
</script>With this map placed in the HTML document before any module execution, the browser can resolve:
<script type="module">
import { debounce } from 'lodash';
import { formatDate } from 'utils/format.js';
</script>The browser automatically rewrites lodash to the
specified CDN URL and utils/format.js to
/src/utils/format.js.
Key Features of Import Maps
1. Direct Module Mapping
You can map an exact string to an exact resource:
{
"imports": {
"three": "/vendor/three/build/three.module.js"
}
}2. Path Mapping (Trailing Slashes)
By ending both the key and the value with a forward slash
(/), you create a path prefix map:
{
"imports": {
"components/": "/scripts/components/"
}
}Importing components/Modal.js dynamically resolves to
/scripts/components/Modal.js.
3. Scoped Mapping
Import maps support a "scopes" key to handle version
conflicts or module-specific dependency trees. If a specific folder or
package requires a different version of a library, you can configure it
independently:
{
"imports": {
"shared-lib": "/libs/shared-v2.js"
},
"scopes": {
"/legacy-app/": {
"shared-lib": "/libs/shared-v1.js"
}
}
}Any file imported within /legacy-app/ that requests
shared-lib will receive version 1, while all other files
will receive version 2.
Rules and Limitations
- Single Map Rule: Browsers currently allow only one
<script type="importmap">per HTML document. - Placement: The import map script tag must appear
before any
<script type="module">tags that rely on its definitions. - No Inline Code: An import map script block must contain only a valid JSON object.
Advantages of Using Import Maps
- Zero-Build Development: Developers can write modern, modular code and import third-party packages from CDNs or local directories without installing Node.js or bundlers.
- Centralized Dependency Management: Package versions and paths are updated in a single HTML block rather than across dozens of individual JavaScript files.
- Standardized Browser Support: Import maps are supported natively across all modern evergreen browsers (Chrome, Edge, Firefox, and Safari), making them a production-ready standard.