There are already good resources for writing vanilla Web Components.

I write mine using lit. I like lit a lot.

This post is part thoughts on using lit and Web Components for a year. Part field notes on the particulars of using lit.

My history with lit

I’ve been using v1 of lit, or “lit-html” as it was known, for a number of years. I’m not a sophisticated ‘lit’ user by any stretch. Principally, in the day job, I build prototypes of product features.

I have used lit as a lightweight tool accomplish those ends. I hate Facebook and so didn’t want to use React. I’d looked at Vue and it didn’t appeal. lit seemed pliable and light enough to allow using template literals for templating and handling DOM updates with efficiency; it excels at updating only the parts of the DOM it needs to. Also, I have really appreciated the little conveniences it provides, such as the ability to do easy click handlers like this:

html`<button type="button" @click="${this.otherFunction}">Fires function when clicked/touched</button>`

That’s just enough abstraction for me. Abstractions like @click are enough to be useful and save a bunch of boilerplate, but not so much that I can’t see what it is actually doing for me.

Skip forward a few years and ‘lit’ as it is now known (not ‘lit-html’) is at version 3, and has a new home at lit.dev.

Crucially, it is now all in on Web Components. I’d been watching Web Components for a while, and waiting for an opportune moment to jump in.

Crucially, I was interested in:

  • Native scoped styling. I was using ecss.benfrain.com for that, and while it works well, something native appealed
  • The ability to make real, actual components. Not some framework based component. Again, something close to the metal appealed to me. Maybe not at the bare metal, but close

Why Web Components?

I struggled to find compelling reasons to look at Web Components until they were supported in all major browsers. Once that hurdle was, largely crossed – as you might imagine Safari is a holdout in some areas – it opened up the idea that in the day job, where I’m prototyping designs non-stop, ‘off the shelf’ parts of our interface, that change very little, could be abstracted to real components, that we could drop into our work, allowing us to work on the things that are actually unique to the feature in question.

So, imagine being able to pull in things and use them like this:

<header-main></header-main>
<main-nav></main-nav>
<sidebar-responsive></sidebar-responsive>
New amazing stuff here which is the actual thing you want to be working on
<footer-main></footer-main>

Typically, each of those Web Components (incidentally, they are always written in kebab-case) may contain 100s of lines of HTML, with associated styling and scripts, but it’s something I wouldn’t need to mess with. I’d be pulling them from some shared library that means they would be kept up to date in one place. That was the hope.

I’ve been down this path many, many times. Abstraction. DRY. All that stuff. I always end up with the same conclusion. The benefits are hugely overstated. But, I was prepared to go around it ‘one more time’.

I was also interested Web Components outside of the day job for applications like WordPress posts. Imagine being able to just slap a Web Component into a blog post, kind of like you can if you use MDX – but without any Facebook tainted code required.

So what has the reality been?

Imagine shimmering music and wobbly visuals and come with me back to the present day…

A year or so on

Naming things has benefits beyond style isolation

It turns out that after a year or more down this path, I find the ability to isolate styles with Web Components overrated. It was a solved problem and if it was already a solved problem for you, the only ‘benefit’ is being able to use html elements without classes for templating and styling. Let me tell you. Component or no component, having everything named merely with the element gets old fast.

I’ve gone back to giving things classes rather than using elements for styling. The ergonomics of searching your codebase for h3 is far more burdensome from a DX perspective than grepping for in-InfoBox (or however you like to name stuff). It also just makes more sense when you template to give thing names, rather than just p & span, you can have in-GameScore and in-TeamName. Looking at a DOM of well named (via classes) elements is just vastly preferable. YMMV.

A ‘library’ of off the shelf components

I can’t tell you the ‘shangri la’ of (any!) ready made components has materialized. I’m unsure if that’s my failing, implementation wise, or a discrepancy between my reality and the implied reality you’re led to believe other devs in the Twitter-sphere enjoy.

I think it’s the same problem I have with design systems. The idea is great. The reality, on any sufficiently large and necessarily complicated UI, is that it is a fools errand. There are too many variants. Not through sloppiness; through necessity. Practically, it makes more sense to go and grab one you made before and amend to suit than shoehorn it into some idea of what the design system wants it to be.

Web Components outside of a project?

I don’t think having a toolchain like lit and Vite to create an occasional Web Components for something like a WordPress post is worthwhile.

If that ever comes along, I’d likely just look to write it vanilla. There are real practical benefits there I can see but I haven’t actually done this yet. So I’d probably simply say no more on the matter for now.

Lit and Vite are great

Lit was great before it used Web Components and using Web Components as its basis now does not diminish its usefulness. It is stable, well documented, fully featured, light and subsequently fast.

Alongside Vite as the build tool I enjoy a very powerful toolset to express reactive user interfaces. Again, I live in prototype land. I cannot speak to using either tool in production environments.

I heartily recommend them. If you find yourself trying to find tools to accomplish similar goals, consider this a glowing endorsement from me.

General ‘field notes’ on using lit day to day

What follows are general ‘notes to self’ on using lit day to day. They don’t necessarily follow logically – sorry about that – but future travellers, including myself, may find them useful.

Importing other elements into an existing component

Suppose you have a app.ts file as the entry point to your app. And you have made yourself a <header-main> component you want to bring in. You do something like this (depending where it is relative to your app file): import "./header/header";

And then use it in your template like this, passing in anything you need for the header-main as attributes:

render {
        html`
        <header-main
            .stateFromApp=${this.stateFromApp}
        >
        </header-main>
        `
    }

Adding CSS from a separate file

In lit you can create your CSS as a string in a separate file and then bring it in to your component. So, suppose in our ‘header’ we want to add styles but write them in a separate file.

Our ‘headerStyles.ts’ file would look like this:

import { css } from "lit";
export const headerStyles = css`
    :host {
        font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue",
            "Segoe UI", Tahoma, Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans",
            "Open Sans", sans-serif;
    }
    /* all styles relevant to header. BTW :host is the root of the component */
`;

And in the header component where you want to include these styles we would do this at the top of the file:

import { html, LitElement, svg } from "lit";
import { customElement, property } from "lit/decorators.js";
import { headerStyles } from "./headerStyles";

@customElement("header-app")
export class HeaderApp extends LitElement {
    static styles = [headerStyles];
    // more stuff...
}

And they would then get automatically included in the generated Web Component. That means complete style encapsulation – so call class names whatever you like and you’re far less likely to get a naming collision!

With that said, I’ve tried this for a couple of months and I’m not convinced it’s actually that useful. I never got collisions using ECSS and the lack of discipline leads to elements named shoddily. So while in my first run out I named things with reckless abandon, and ended up with things named mainBtn alongside btn-secondary, for example. Now I opt for a ‘ECSS’ style naming convention and just drop the namespace section. So, something that would have been mn-MainBtn will just be MainBtn. I’d advice anyone else to still adopt some kind of naming convention, whatever their preference in that regard is.

But read on for a better way to include your styles.

Styling a lit component with Sass via vite

Big shout out to Tom Millard for sussing this approach out – it isn’t/wasn’t officially documented when we started using it.

I’d all but given up on the notion that I could write styles in Sass and get them included in the Web Components that lit creates. Writing styles in template literals isn’t the end of the world but it means you miss out on syntax highlighting and autocomplete goodness in your editor.

Thankfully, this is possible and is a near perfect experience.

I’m using Vite as my build tool of choice at present. One benefit of that is that you can bring in Sass files as imports into your JS/TS. And then in turn lit can consume them. I find this makes for an infinitely more desirable authoring experience.

I understand that lit will make this process/approach ‘better’ going forward as it relies on using the, worryingly titled, unsafeCSS() method. But as we were just kicking the tyres of lit, Vite and Sass together, we didn’t worry about security concerns.

How to use Sass in lit Web Components

You import your Sass into your component at the top of the file with this kind of syntax:

import styleImport from "./your-component.scss";

And then you use it like this:

export class YourComponent extends LitElement {
static styles = unsafeCSS(styleImport);
// More

And that injects the converted Sass into the component. Almost perfect – except that as I write this, it doesn’t do live reload/HMR when you save but that will likely be a solved problem in the coming months.

Using SVGs in lit Web Components

If you want to inline SVGs in your web components, sticking them inline in the root and using <use> isn’t an option, due to the shadow DOM boundaries. Two choices here. Either stick the SVG into a template literal like this:

import { svg } from "lit";
export const yourSVG = svg`<svg width="27" height="15"...></svg>`;

And then import them and use them like this:

import { yourSVG } from "../YourLocation/YourFile"

And then use them in the template like this: ${yourSVG}.

Or you can use the unsafeSVG method. That means you can keep your SVG as a standard SVG file in your file system. You will need to import that like this:

import { unsafeSVG } from "lit/directives/unsafe-svg";

Then bring in the SVG asset like this:

import yourSVG from "../img/yourSVG.svg?raw";

And then use it in your template like this:

${unsafeSVG(yourSVG)}

Both these approaches end up with the SVG in your HTML just the same so its largely a preference thing. Although with the raw method, you do get to keep your SVGs as SVGs in your file system, which may be preferable. There are perhaps security concerns but that is beyond my expertise currently.

Selector usage in Web Components

One thing that may catch you out relates to Sass and nesting selectors. Suppose, you have a rule for the host that looks something like this:

:host(your-component) {
    // Other styles
    transform: translateY(100px);
    transition: all 0.2s 0.3s;
}

And based upon an attribute change on that host, you want to change that transform in Sass.

What you can’t do is this:

:host(your-component) {
    // Other styles
    transform: translateY(100px);
    transition: all 0.2s 0.3s;
    &[data-thing] {
        transform: none;
    }
}

That will compile to a selector as you might expect of Sass:

:host(your-component)[data-thing] {
  transform: none;
}

But that isn’t how the :host selector works. The attribute needs to go inside the parenthesis.

The thing to be aware of with the & in Sass is it can be thought of as an ‘additive’ – it adds strings to the original selector. In this case we want to actually want to amend that original selector. There is no way to do that in Sass so we need to write it correctly ‘long hand’ after the first rule:

:host([data-thing]) {
    transform: none;
}

Basic reactivity

The understanding I have of lit when it comes to reactivity is that you pass data and state down through components and when you want to update that data, it works best to send those changes back up with an Event.

Suppose you have some data. And you want to pass that down to a component nested a few levels deep. This can be achieved by importing the data with an import at the outermost component:

import { data } from "./dataStub";

You then set this to be state:

@state()
datad = data;

So now datad is our ‘reactive’ version of the data. The original data will be unchanged from now on.

Now we pass that data down to a nested component like this:

<nested-comp
@dataupdate=${this.handleDataClick}
.datad=${this.datad}
></nested-comp>

Ignore the @dataupdate in there for now!

Then in the nested component, ‘nested-comp’, within the class, we wire that data up like this:

@property({ type: Array })
datad: Array<Object>;

Then we can either access it in our template with this.datad or we could pass it down again to another nested component the same way (.datad=${} on the component and the @property on the next nested component).

So that’s passing it down, what happens when we want to press one of our buttons and send the selected state back up? And how can any other components then also listen to these changes?

To do that, we use Events. So on your nested component, we attach an event like this:

${datad.map(
(choice) =>
    html`<button
        type="button"
        @click=${() => {
            this.selectThis(choice);
        }}
        selected=${choice.selected}
    >
        <span class="name">${choice.name}</span>
    </button>`
)}

Then you want the function that the click fires to do something like this:

selectThis(choice: any) {
choice.selected = !choice.selected;
const options = {
    detail: this.datad,
    bubbles: true,
    composed: true,
};
this.dispatchEvent(new CustomEvent("dataupdate", options));
}

And that sends that Event up through the components towards anything that cares to listen. Remember the @dataupdate=${this.handleDataClick}? Well that is how Lit listens for Events you send up. So when we dispatch that dataupdate Event, it is heard up the tree, and we can run the handleDataClick function, in our ‘app.ts’, off the back of it. That function can do whatever changes it needs on the data and then the changes propagate back down. I’ve found if you are amending some part of say a nested object or array of objects you need to completely replace the array or object. That seems simplest with a spread so it would look something like:

this.data = [...this.data];