Skip to main content
Huddle's Dev Thoughts

Building a Simple Lit Component

Approximately 5 min read time

So, you think you want to get into Lit, but diving into a whole new framework feels like effort, and you wish someone would give you some simple to follow instructions to make life easier. Well, there's the Lit Getting Started page, but since you're here, lets assume you'd prefer me to walk you through it, or you're after a less "clinical" approach to setting up a project.

Project Setup

#

First things first, we're going to want a project to work in. So,

$ mkdir lit-component-project

$ cd lit-component-project

$ npm init

$ npm i vite lit typescript

We make a folder (you can replace lit-component-project with whatever floats your boat right now), cd into it, init an npm project in that folder, and install a few key dependencies.

Then you'll want to make a few files:

And there you have the absolute barest minimum setup to start up a typescript + lit project. All that's left is to start it up and bask in all that amazing web-component functionality.

$ npx vite dev

If you open a browser to the url vite helpfully printed to the console, you should be able to see Hello World as clear as javascript. If you desire, you can alias that command as an npm script in your package.json as "start": "vite dev" so that all you have to run is

$ npm start

A Brief Explanation

#

At this point, you might be asking a few questions, such as:

Q: (If you're coming from the world of webpack): Where's my enormous config file?

A: vite has sensible defaults. By default, it looks for an index.html in the current directory, and goes from there. Installing typescript as a dependency is enough to support typescript files. If you wanted to use sass/scss or some other css preprocessor, all you would need to do is install the processor, import a file w/ that extension, and off you go. You can also provide it with a config file (it even accepts a config written in typescript, provided you have it installed) in order to provide it with specific plugins, or overrides to its defaults, or other things.

Q: Holy crud this is amazing?

A: Not technically a question, but yes, you are correct.

Now, being able to print "Hello World" with a single tag is great and all, but what if this component had some actual functionality? As for the functionality in question, I'm going to go with what I asked ChatGPT to write for me in the previous post.

To recap, what we're after is the following:

  1. a clickable component
  2. that counts how many times it's been clicked
  3. and displays that somehow

Even More Code

#

Here's a simple way to achieve exactly that:

The Whole Explanation

#

You've seen the index.html before, so ignore that it's importing index.js instead of index.ts. That's a function of using the playground-ide for these live code demos. In a standard vite project you'd import index.ts like normal. There's a css file here too, but we'll get to that later.

All the magic happens in index.ts.

First up, there's the @customElement decorator. You could customElement.define('tag-name', constructor), but the lit @customElement decorator does that for you, and in a nicer fashion than having to remember to call customElement.define.

After that is the static styles property, with the css tagged template function. This takes straight css, and applies it to every instance of this custom element via a shared style sheet. This means there's only 1 copy of these styles in the browsers memory at a given time, and is far more efficient than inlining a style tag inside of your component. static styles can also be an array of multiple CSSResults, so you could define some css that is common to all of your components in another file, import it into each component and apply it via the static styles. It really is straight, normal css though. The one "new" thing to note is the :host selector, which targets the my-counter-component element in this case, but in general is the root element of your custom element (the tag right outside of the shadow dom). Note that any styles applied to :host are overridable from the outside, because they exist outside of the shadow dom.

Next come the @state and @property. These both do nearly the same thing, with the only real difference being @state is internal, and @property is external (and provides the ability to set the property from the outside via either an attribute or the property). Try editing the html above and providing the name property on the my-counter-component. These both set up machinery, so that when they are set or modified, lit knows that it needs to rerender (and it's smart enough to batch the updates if a bunch of them happen in a burst).

Finally there's the render function. This returns the actual contents of the component via the html tagged template. This takes, you guessed it, straight up html. Attributes are bound as normal, properties can be bound directly (for rendering nested components and whatnot) by prefixing the name of the thing with a . (a period), boolean attributes can be present or removed by prefixing them with ? (a question mark), and event listeners can be bound with an @ (an at-sign). Since this is a tagged template string, you can also use normal template string mechanics to template in whatever you feel like.

Event handlers are a little special in that you bind them directly to functions. You can either pass an inline arrow function, or you can pass a reference to an instance function via this.functionName. Here, the code binds a simple arrow function since all its doing is incrementing the count state by one. As mentioned before, this will trigger a re-render.

The final thing you might've noticed is that part attribute. This is part of the web component spec. Shadow DOM hides the internals of a web component from the outside, and prevents styles from leaking in or otherwise being applied to elements inside the web component. CSS Parts let you expose specific peices of your component to the outside, in an almost api-like fashion. You can read more about CSS Parts here but they don't allow access to a part's children, or siblings, but do allow access to pseudo-elements on the matching parts. You style them from the outside by using the ::part(part-name) pseudo selector on the custom element tag name or a selector that matches the custom element. In this example, I increased the padding and font of the button as well as giving it a hover effect, and all from outside the component itself.

The Wrap Up

#

Hopefully you should be able to see that making, using, and sharing components in Lit is quite simple. There's almost no boilerplate to a component, styling and rendering is super simple, and lit handles all the heavy lifting of wiring up attributes, listening for changes to trigger a re-render, efficiently binding event listeners and so much more.

This post has only scratched the surface of the world of lit components, and if you're interested I would absolutely recommend going to lit.dev and reading through the docs there. They're super helpful, and they have a number of live examples using the exact same technology that I've used here (the playground-ide components I'm using here were written in lit, presumably for the original purpose of having a way to demo lit live on the web.)