HTML module script: split a working page into files with import and export

Use <script type="module" src="./main.js"></script> to load a JavaScript module from HTML. Export shared functions from another file and import them by URL, including ./ and the file extension. Serve the folder through an HTTP development server; opening a module project by double-clicking its HTML file commonly fails because of module security requirements. MDN JavaScript modules

A workshop quote page has begun using the same calculation in its price panel and its printable summary. Keeping two copies has already produced different totals. Moving the calculation into one module gives both consumers the same rule without putting the function on window.

Keep the first split small

Create three files in the same folder:

quote/
  index.html
  main.js
  pricing.js

In index.html, keep the visible result and load the entry module:

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Workshop quote</title>
<h1>Workshop quote</h1>
<p id="total">Calculating…</p>
<script type="module" src="./main.js"></script>
</html>

In pricing.js, export one calculation. This illustrative quote uses a $45 setup charge plus $12 for each seat:

export function quoteTotal(seats) {
  if (!Number.isSafeInteger(seats) || seats < 1) {
    throw new RangeError('Seat count must be a positive integer');
  }
  return 45 + seats * 12;
}

In main.js, import the named function and display the quote for eight seats:

import { quoteTotal } from './pricing.js';

const dollars = quoteTotal(8);
document.querySelector('#total').textContent =
  `8 seats: $${dollars.toFixed(2)}`;

The expected line is 8 seats: $141.00: $45 plus $96. The browser loads pricing.js because the entry module imports it. Do not also add a separate classic script tag for that file.

What changes when a script becomes a module

Module declarations have their own scope. A top-level function in main.js does not automatically become window.someFunction. An old HTML attribute such as onclick="recalculate()" can therefore fail after the move. Attach the listener inside the module with addEventListener, where the function is in scope. The HTML events guide explains that change.

Modules use strict mode automatically. The normal module tag also waits until the document has been parsed before execution, so this entry module can find #total even when its tag is placed in the head. Adding defer does not add another delay. async changes loading behavior and should be omitted for this simple dependency setup. Module execution differences

Named imports must match named exports. Here, import quoteTotal from './pricing.js' would request a default export that the file does not provide. Keep the braces in both examples unless you deliberately change to a default export.

Resolve each path from the file that contains it

The HTML src resolves from the document URL. An import in main.js resolves from that module's URL. If you move main.js into scripts/ while leaving pricing.js beside the HTML, update its import to ../pricing.js.

In a browser, import { quoteTotal } from 'pricing' requires an import map or a build system that resolves the bare name. A nearby file named pricing.js does not make that bare name work. The relative URL is sufficient for this project; a package manager is unnecessary.

Diagnose the request that actually failed

Observation Check next
Import rejected at a file:/// address Open the folder through the HTTP development server you use for the project
Network response is 404 Check the imported path relative to its importing module
JavaScript MIME type error Inspect the response headers and body; an HTML fallback page is not a JavaScript module
Export name error Compare the exact imported name with the exported declaration
Inline click handler cannot find a function Move the handler registration into the module

Cross-origin module files also need appropriate CORS approval from their server. Renaming a file from .js to .mjs alone fixes neither response headers nor cross-origin permission. For wider failures, use the JavaScript loading checklist.

Before handing over the quote project, open the actual served entry page, confirm the $141.00 result, and inspect both JavaScript requests. Deliver the folder or built output with its dependencies. Sending only index.html loses the calculation file that now owns the pricing rule.

Keep reading