It’s an established Good Idea™ that building digital interfaces of any kind is best done by building components and then piecing together the interfaces from those components. This can be sliced and diced a lot of ways, but generally: a component is a reasonable independent peice of what that interface needs. When it comes to websites, things like a header, footer, grid, card, button, etc. A design system, as it were. See concepts like Atomic Design.
A nice by-product of the Rise of JavaScript Frameworks is that they solidified this idea. React, Vue, Svelte… you work with them by building components and composing them together. That’s their point.
I like the idea of userland tools like JavaScript frameworks pushing the boundaries, then the web evolving to not require those tools. So can we pull off a component-structured project without any build process or framework? We’re close.
This is an example of how I’d like to structure a website:

Those components (in our simple example, a button, card, and header) are all:
- Inside a
components
folder, each with their own named folder (organized!) - Have a file for their template and logic
- Have a separate CSS file
So like this:

This is the kind of logical grouping and isolation that makes sense to me in creating a component architecture. A more complex setup might have components with, say, .graphql
files, their own images, tests, etc. The co-location is key to sanity.
Our components are JavaScript here because there is no concept of HTML includes yet, but also that web components are a generally nice way to handle this anyway, and they require JavaScript instantiation. We don’t need any framework to use web components (hence “vanilla app architecture”), but in the demo, I’ll use Lit (just a light helper library).
How do we integrate those component.js
and component.css
files? That question has long lingered for me. Bundlers can do this job. For instance, webpack just invented their own way of dealing with it. If you type import "./card.css";
in a JavaScript file that is processed by webpack, it’ll just know what you mean and ensure that CSS is loaded on the page somehow. Likewise, Vite just does it’s own thing:
Importing
.css
files will inject its content to the page via a<style>
tag with HMR support.
That’s great and all, but we’re trying to go vanilla here. No bundler/build process. How do we import CSS like that?
Enter CSS Module Scripts
Good news: JavaScript has an answer to that question we just asked, and it’s called CSS Module Scripts.
Bad news: Only Chrome supports it. (WebKit bug; Firefox bug)
Google’s blog post on them (linked above) is one of the few pieces of information available about them, and it contains some incorrect syntax, so be careful there. It should look like this (the with
keyword is correct, if you see assert
that’s old/wrong):
import sheet from './styles.css' with { type: 'css' };
Code language: JavaScript (javascript)
When you do that (in a supporting browser), sheet
becomes a “Constructable Stylesheet” and then you can use it to, in our case, apply it to the Shadow Root of a web component.
class MyComponent extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.adoptedStyleSheets = [sheet];
}
...
Code language: JavaScript (javascript)
These “import attributes,” as I think they are called, can do other things. It’s much better supported to import JSON this way, like:
import sheet from './data.json' with { type: 'json' };
Code language: JavaScript (javascript)
Lit
Using Lit, applying the styleheet (or, “the constructable stylesheet, as imported via CSS module scripts” to do the whole mouthful) is like this:
import {html, LitElement} from 'lit';
import sheet from './button.css' with { type: 'css' };
class My Component extends LitElement {
static styles = [sheet];
...
Code language: JavaScript (javascript)