As of ES6 (ES2015), JavaScript supports a native module format called ES Modules, or ECMAScript Modules. This is modern way to do modules in JavaScript.

This approach uses the export and import keywords, instead of the older CommonJS syntax of module.exports and require.

This page covers more details on the syntax and examples of loading modules in different situations.


This approach is widely supported:

  • Browsers (including Chrome, Firefox and Safari since at least 2018).
  • Node.js
  • Deno

For browsers which don’t support it, you can use a fallback:

<script type="module" src="main.js"></script>
<script nomodule src="fallback.js"></script>


Enable ESModules

Set script tag type

Set the type of script tag to module.

This works for an inline script or linking using src.

Keep in mind for the sections later on this page that you can use either approach. Having your JS in a file or files separate from HTML can make JS easier to manage.

Inline approach

  • index.html
      <script type="module">
      import { bar } from "";
      import { foo } from "./foo";

External approach

  • index.html
      <script type="module" src="main.js"></script>
  • main.js
      import { bar } from "";
      import { foo } from './foo';

Set package type

Configure NPM to see your project as ES Modules.

  • package.json
        "name": "my-app",
        "type": "module"

Warning - if you have a config or ESLint or similar which uses the old module.exports syntax, you’ll get an error and so you’ll have to rewrite that config. VS Code provides a neat feature to do that for you.

Set file extension

Use .mjs extension in place of .js. This is not so common, but it allows you do mix both types in a project which does not have the NPM config set to use the module type.


When loading a JS library from a CDN, make sure you pick a URL which is compatible with ES Modules approach. This might mean a param like ?module or loook for .mjs extension.

See my JS CDNs guide.

Scoping note

Note that imports are scoped to where they are used. So after script runs, you cannot access it in the dev console. But the plus side is that you can don’t have to worry about namespace collisions of different packages. And you can even import two versions of say React on the same page and use them independently in two script tags.

Import sources

Import local JS module

Import from a local module. Note dot slash.

import { foo, bar } from './foo';
import { buzz } from '../bazz/bizz';

Often the extension is omitted - like .js, .ts or .jsx. You might have to include if .vue.

Import type definitions

For use in TypeScript.

import { IFoo } from './foo.d';

Import an asset


Load a CSS file so that it gets added to the page. Note you don’t have to specify an object to import or how it will be used on the page.

import './index.css';


Load a image as an object so you can use it as a URL in your content.

Based on React Quickstart.

import logo from './logo.svg';

export default function App() {
  return (
    <div className="App">
      <img src={logo} />

Import a 3rd-party package

From Node modules

Import from a package installed with NPM.

import * as vscode from "vscode";
import * as assert from "assert";
import React from 'react';

Or if you’ve aliased a package name to a URL with import maps or a deps.ts file (Deno).

From a URL

Import from a URL of a package - such as on GH, NPM or a CDN.

import React from "";

The browser will download that script for you. No NPM needed.

This means can run your JS code consistently on the server-side and in the browser. At least with Deno. I don’t know about Node.

Normally a JS script on the server side would have no awareness of the HTML tag to load JS using script tag with src.

This is also the default approach for Deno.

Local modules

In an NPM project, you can configure your project to use ES Modules as follows.

  • package.json
        "type": "module"

In testing, I found that my ES Module imports gave an error without this config value setup.

A hybrid approach is possible as in Vue Quickstart, when omitting the config value above. The configs at the top-level using module.exports syntax but the src directory modules use the ES Module syntax.

Named exports

Some ways to export with a name.

  • foo.js
      function foo() {
      export { foo };
  • foo.js alternative.
      export function foo() {


import { foo } from './foo';

// Multiple.
import { foo, fuzz } from './foo';

// Rename.
import { foo as fizz } from './foo';

Or, exporting a module (without importing it first).

  • foo.js
      export { foo } from './foo';

Import multiple objects and put them under a namespace.

import * as foo from "./foo";

Default exports

  • foo.js
      function foo() {
      export default foo;


import foo from './foo';

Combine multiple defaults


export React from "";
// And.
export ReactDOMServer from "";


export { default as React } from "";
export { default as ReactDOMServer } from "";

Now you can import from that module.

import { React, ReactDOMServer } from "./deps.ts";

Mixing named and default imports


// Default.
import React from "./deps.ts"
// Named.
import { Application } from "./deps.ts";


import React, { Application } from "./deps.ts";


In case a browser does not support ES Modules, you can prompt the user to upgrade.

Example - based on homepage source.

<script module>
    // ...
<script nomodule>
    const mainEl = document.querySelector('main');
    mainEl.innerHTML = '<p><em style="color: #999;">nomodule, please upgrade your browser...</em></p>'