How to import CSS Files from an ESModule in 2023

A friend asked me some days ago how he could import a css file relative to an ESModule js file in a browser, which got me on a little journey!

The Idea
ESModules can import other ESModules
by simply importing them statically (via the import {x} from 'package' syntax),
or by importing them dynamically (via import(packageUrl)).

How can we apply this to load pure CSS files?



  1. Step 1 - If you need something stable for today, use import.meta.url in the js module as a base for the css url, then load the css via a style tag inserted into the dom.

  2. Step 2 - If you don’t need Safari right now: Use Constructable Stylesheets together with fetch()!

  3. Step 3 - If you want to be even more adventurous, use the new CSS Module Scripts Syntax, which is only available in Chrome and Edge at the time of writing.


Prerequisites - Getting the url of the current ESModule js file

To load a css file relative to the currently loaded ESModule, we need the url to the folder of the currently loaded file first. (duh :D ) In node, we would use __dirname or __filename. In an ESModule we can access it via import.meta.url:

// inside the current ESModule file
const myFileUrlString = import.meta.url;

// simply construct a new relative URL like this:
const mySiblingCssUrl = new URL("mySibling.css", myFileUrlString);

// Explanation: The second param of the URL constructor is used as the base URL for the first parameter.
// This can be used for navigation relative to the current ESModule!
const myGlobalCssUrl = new URL("../../assets/global.css", myFileUrlString);


With the relative path to our css generated, we can simply add it into our document like this:

// Get HTML head element
const head = document.querySelector("head");

// Create new link Element
const link = document.createElement("link");

// set the attributes for link element
link.rel = "stylesheet";
link.type = "text/css";
link.href = mySiblingCssUrl.href;

// Append link element to HTML head

But this way of importing css is not optimal. First, it forces us to construct another dom element, insert it into the dom and therefore stringify the resource into the dom. Second, it forces the browser to parse the new dom element, go on with fetching the target href, dowloading and parsing the css file and finally rendering all sections of the dom which are changed by the new stylesheet!

Sad 😥


The (near) Future: Constructable Stylesheets 🤩

Fortunately, a solution is comming!

This is called ‘Constructible Stylesheets’ and is currently available in Chrome, Edge and Firefox! (No Safari yet 😢) The basic idea is: What if we could take any string of css and instruct the browser to parse this on demand and use it where we need it?

Here’s a snippet how this could be used with fetch!

// construct a new url to your css file
const myTargetCss = new URL("myTarget.css", import.meta.url);

// directly fetch its content
const cssContent = await (await fetch(myTargetCss)).text;

// construct a new stylesheet
// => aka Constructible Stylesheet! 🤯
const sheet = new CSSStyleSheet();
await sheet.replace(cssContent);

// adopt the stylesheet objects to a document ...
document.adoptedStyleSheets = [sheet];

// ... or to a shadow root
const node = document.createElement("div");
const shadow = node.attachShadow({ mode: "open" });
shadow.adoptedStyleSheets = [sheet];

The Cool Parts

Some Gotchas


The coolest Future: CSS Module Scripts! 😍

Here comes the coolest thing: CSS Module Scripts!

But what should that be, you ask (slightly annoyed)?

Lets take a step back and look at an option webdevs have known for a long time:

Webpack Loaders

With webpack loaders one can tell webpack how non-js filetypes should be loaded at build- or even at runtime! So we had many loaders, for json, svg, css and many more.

With these loaders we could import css like this:

// in the js file
import css from "file.css";

This looks great! However, since it’s not standard syntax, it would not work in browsers until we do this:

// install the webpack loader package
npm install --save-dev css-loader

// in the webpack.config.js
module.exports = {
  module: {
    rules: [
        test: /\.css$/i,
        use: ["style-loader", "css-loader"],

This sounds way less exciting now, especially when thinking about the rest of the webpack config! 😞

CSS Module Script Imports to the rescue!

Browsers are working on a new Syntax for importing non-js files: The so called Import Assertions:

// Woha, whats that?!? We can import css directly into an ESModule?
import sheet from "./styles.css" assert { type: "css" };

// The type of sheet is a normal constructible stylesheet,
// so it can be adopted to any document or shadow root!
document.adoptedStyleSheets = [sheet];
shadowRoot.adoptedStyleSheets = [sheet];

With them, we can simply define the type of an import and get a specialized object out. In this case, it’s a CSSStyleSheet object, the same as in the previous example! So, with this capability you could simply import your css file relative to your esmodule and instantiate it via constructable stylesheets!

Behind the Scenes: Import Assertions - TC39 Proposal

The exciting news is now the arrival of the Import Assertions Proposal at Stage 3 in the TC39 process for new JavaScript features. Import Assertions do not only define such an assertion for css, like

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

but may be used more formats, including JSON, Webassembly (in discussion) and potentially HTML!
For more in-depth info, look at this github repo: tc39-proposal: import assertions.


Further Reading

These are some useful links where you can read more about all those technologies!


Follow me for more!

I’ll keep watching these developments and if you want to be informed of changes in this regard, follow me on

{/_ or subscribe to my RSS Feed! _/}

Thank you!