Using JavaScript Import Maps Without Bundlers
Import maps provide a native browser mechanism to control how
JavaScript resolves module specifiers in standard ES module
import statements. By defining a JSON structure inside an
inline HTML script tag, developers can map bare module specifiers,
directory paths, and specific scopes directly to remote or local URLs.
This enables modern web applications to manage modular JavaScript
dependencies directly in the browser without requiring a pre-processing
or bundling step with tools like Webpack, Rollup, or Vite.
The Problem with Native ES Modules
Standard ES modules natively supported in browsers require explicit file paths or full URLs for imports, such as:
import { add } from './math.js';
import confetti from 'https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.4/+esm';Unlike Node.js or bundler-driven workflows, browsers cannot natively
resolve “bare specifiers” like
import confetti from 'canvas-confetti' because they lack
access to a local node_modules directory lookup algorithm.
Without import maps, developers either had to use bundlers to resolve
these paths ahead of time or rewrite import URLs manually across all
files whenever an endpoint changed.
Declaring an Import Map
An import map is declared in an HTML document using a
<script> tag with type="importmap". It
must be placed before any module scripts that rely on its mappings.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Import Maps Demo</title>
<script type="importmap">
{
"imports": {
"canvas-confetti": "https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.4/+esm",
"app/": "./src/"
}
}
</script>
</head>
<body>
<script type="module">
import confetti from 'canvas-confetti';
import { logger } from 'app/utils.js';
confetti();
logger('Confetti fired!');
</script>
</body>
</html>Key Mapping Features
1. Bare Module Specifiers
The most common use case is mapping a single package name to a specific CDN URL or local path.
{
"imports": {
"lodash": "https://esm.sh/lodash-es@4.17.21",
"react": "https://esm.sh/react@18.2.0"
}
}This allows code to use import _ from 'lodash' without
runtime resolution errors.
2. Path Prefix Matching (Trailing Slashes)
By appending trailing slashes to both the key and the target path, import maps allow dynamic resolution of sub-paths or directories.
{
"imports": {
"components/": "/static/js/components/",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}Importing components/Button.js dynamically resolves to
/static/js/components/Button.js.
3. Version Scoping with
scopes
When different parts of an application or third-party dependencies
require different versions of the same library, the scopes
key applies overrides restricted to specific module URL paths.
{
"imports": {
"chart.js": "https://esm.sh/chart.js@4.4.0"
},
"scopes": {
"/legacy/": {
"chart.js": "https://esm.sh/chart.js@2.9.4"
}
}
}Any script loaded from within /legacy/ that imports
chart.js receives version 2.9.4, while scripts loaded
elsewhere receive version 4.4.0.
Benefits and Modern Browser Support
Import maps are natively supported across all modern browsers (Chrome, Edge, Firefox, and Safari). Key benefits include:
- Zero-Build Prototyping: Build applications and utilize modern libraries without maintaining complex build tool configurations.
- Centralized Dependency Management: Change library versions or CDN providers in a single location rather than updating paths across multiple source files.
- Cached CDN Dependencies: Leverage HTTP/2 or HTTP/3 multiplexing and external CDN caching directly via standard ES module workflows.