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.
vite
- this is our build tool, almost zero config, simple, fast, modern, I wouldn't use anything else nowadays to be honest (vite docs)lit
- this is where all thelit
code lives, we need this to write anything inlit
typescript
- let's be honest here, Types are Good™. I don't write Javascript anymore, only Typescript, and so we'll be using Typescript here
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:
- a clickable component
- that counts how many times it's been clicked
- 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.)
- Previous: Hello World
- Next: Comparing Lit and FAST