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', function() {
        const jsonmodule = await import('stuff.json', { with: { type: 'json' } });
        console.log(json.default);
    });
</script>

import() returns a promise which fulfills with an object containing all exports from the module. The JSON data is the default export, so we access it with .default. There are no named exports.

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" };
document.adoptedStyleSheets = [...document.adoptedStyleSheets, styles];

We start the array with ...document.adoptedStyleSheets. This stops any other CSSStyleSheet we added via adoptedStyleSheets from being overridden.

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

An article on the web.dev 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.

If you need to, it’s easy to add or delete CSS rules in the CSSStyleSheet before applying it to the document.

import styles from "./styles.css" with { type: "css" };
styles.insertRule(".btn { color: white; font-weight: bold; }");
import morestyles from "./morestyles.css" with { type: "css" };
document.adoptedStyleSheets = [...document.adoptedStyleSheets, styles, morestyles];

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', function() {
    const styles = await import('./style.css', { assert: { type: 'css' } });
    document.adoptedStyleSheets = [...document.adoptedStyleSheets, styles.default];
})
</script>

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

Confusingly, CSS Modules is also the name of a popular open source project for scoping CSS. That is not something that the web standard does, and there isn’t any relation or similarity between the standard and the open-source project. They are sometimes referred to as “CSS Module Scripts”, which might help to avoid the confusion.

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.

Here’s an example using declarative shadow DOM (using the shadowrootmode attribute on a template element means the contents of the template will be put into a shadow tree attached to the parent element).

<div>
    <template shadowrootmode="open">
        <p>This is in the shadow DOM</p>
        <button>Shadow button</button>
    </template> 
</div>   

    <script type="module">
         import styles from "./styles.css" with { type: "css" };
         document.querySelector('div').shadowRoot.adoptedStyleSheets = [styles];
    </script>

Or for a custom element:

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

Or if you’re using Lit to create a web component it would look like this:

import {LitElement, html} from 'lit';
import CSSStylesheet from './my-element.css' with { type: 'css' };

export class MyElement extends LitElement {
  static styles = CSSStylesheet;

  render() {
    return html`
      <h1>Hello world</h1>
    `;
  }
}

window.customElements.define('my-element', MyElement);

Preload

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 already shipped JSON modules and CSS modules using the older syntax (import json from "./data.json" assert { type: "json" }). Chrome Canary has updated to use the new syntax (the old syntax still works, but is deprecated). JSON modules are supported in Safari Technology Preview. The older syntax is also supported in Deno and (experimentally) in Node. Hopefully they will get an update soon. Safari has a positive position on CSS modules and had previously implemented JSON modules with the old syntax in Safari Technology Preview.

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

Bun has already adopted import attributes for macros.