Import JSON, CSS and more with import attributes

If you’ve worked with build tools like Webpack, you’re probably used to a simple syntax for importing JSON and other resources: import data from "/data.json" or const json = require('./data.json'). Browsers have never understood this, but bundlers made it work. Now there’s a web standard.

import json from "./data.json" with { type: "json" };

For security reasons (a file extension alone is not a reliable enough indicator of the content type) you have to specify the type using with {type: "json"}.

The JSON data is the default export. There are no named exports.

The above example imports a JSON module, but the same syntax is used to import other resource types (CSS, possibly HTML and WebAssembly in the future).

Dynamic import()

import() will dynamically load a module so that it is only evaluated when needed. In contrast to an import statement, which must be used at the top level, import() can be used inside a function or inside an if statement.

<button>Load some json</button>

<script type="module">
    const button = document.querySelector("button");
    button.addEventListener('click', async function() {
        const jsonmodule = await import('./stuff.json', { with: { type: 'json' } });

Importing CSS

The default export of a CSS module is a CSSStyleSheet object. Rather than creating a new <link> or <style> element with e.g. document.createElement(), you apply the stylesheet to the document using adoptedStyleSheets.

import styles from "./styles.css" with { type: "css" };

There are no named exports from a CSS module but that might change in the future.

An article on the blog explains some of the benefits of native CSS modules:

  • Deduplication: if the same CSS file is imported from multiple places in an application, it will still only be fetched, instantiated, and parsed a single time.
  • Consistent order of evaluation: when the importing JavaScript is running, it can rely on the stylesheet it imports having already been fetched and parsed.
  • Security: modules are fetched with CORS and use strict MIME-type checking.

As with JSON modules, you can dynamically import a stylesheet:

<button>Add some style</button>

<script type="module">
const button = document.querySelector("button");
button.addEventListener('click', async function() {
    const styles = await import('./style.css', { with: { type: 'css' } });

The CSSStyleSheet is accessed with .default because it is the default export of the module.

Using CSS modules with Shadow DOM

If you’re using shadow DOM you can apply the stylesheet to a shadow root instead of the document.


<script type="module">
import styles from "./styles.css" with { type: "css" };
class MyElement extends HTMLElement {
    constructor() {
        this.attachShadow({mode: "open"});
    connectedCallback() {
        this.shadowRoot.innerHTML = "<p>Lorem ipsum</p><button>Hello world!</button>";
customElements.define("my-element", MyElement);


You can preload non-JavaScript modules with rel="preload" (rather than "modulepreload", which should be used for JavaScript modules):

<link rel="preload" as="json" href="...">
<link rel="preload" as="style" href="...">

Browser support

Import attributes are at stage three. JSON modules and CSS modules are included the HTML spec. Import attributes were previously known as import assertions. They have been renamed and the syntax has changed. Chrome/Edge had shipped JSON modules and CSS modules using the old syntax (import json from "./data.json" assert { type: "json" }). Chrome has updated to the new syntax as of version 123 (the old syntax is deprecated). JSON modules are supported in Safari since version 17.2. Safari has a positive position on CSS modules.

On the backend, import attributes are supported in Deno and Node. Bun has adopted import attributes for macros.

Babel, Webpack and Rollup have all implemented support for the syntax.