Web Components, Lit and Santa 🎅


Get started with Web Components – enabling you to define new HTML elements that work everywhere regular HTML does. This talk will focus on Lit, a suite from Google that helps you create WCs with features you'd expect like data-binding and declarative definitions. It'll also cover how we've used them to build one of the web's jolliest sites, Google's Santa Tracker 🎅


Hey there, my name's Sam and I'm here today to talk about web components, the Lit Library and Santa Tracker. But first, some background on me. I'm lucky enough to be the lead on Santa Tracker. It's something I've done for five or six years now. Luckily, I don't have to do it all year round, only for a few months around the holiday season. Otherwise, I would go insane, just to be clear. I also work on content sites like web.dev and developer.chrome.com. For me personally, what do I like? I like pushing the boundaries doing weird web things. You can read my blog if you are curious about more. And I have some dislikes here, too. I like building websites in a very sudden way. And I think big builds during development really slow you down. I'd love to see a much faster development process. And I think a lot of tools now do help support that. Firstly, we're here to talk about web components. So let's have a bit of background on elements and what pages are. Now, we all know html pages are made up of a bunch of different elements. These can be semantic elements that mostly just affect your style and also are important for things like screen readers and also a bit of semantics for you as a developer, things like header and section. But they can also be elements that have functionality, things like a and button and elements like dialogue and details and more advanced versions of those that actually have a bit of a state that changes. So web components fundamentally, when it gets down to it, lets us write our own elements. These are first class citizens that work just like regular html elements. I'm going to talk through an example of my details, which I'll get to shortly. So what are the goals of this talk? I want you to be introduced to web components and their component parts. This is a fairly high level talk. I want to make that clear. We'll be skipping over some more nuanced things, which hopefully I'll point you in the right direction so you can go find out more. I want you to learn about Lit, a very opinionated Web component library that Google ships, which is really the best place to get started writing web components. And also, I'd be very happy in another tab if you open Santa Tracker and click around and play some games and just try to work out which games and what parts of the site are run with web components. So let's talk through details. Details is a really interesting element that I mentioned before. It's got a bunch of functionality. If you haven't seen it before in action, what it does is it shows the summary text, which is the thing in the top here, and only shows the longer form description when you click on that summary. It makes a lot of sense and it's really handy. I want to write my details. Now this will be very similar to the regular details element, but with one key difference. This is what it will look like as a Web Component. It can't look exactly the same. We have to use this slot thing here, but I'll mention how that works later on. So my fundamental difference of my details versus details is that these details should open upwards. Now this may not be the most important thing on the web, but I thought it was a fairly cute example to show just how powerful web components are, that I can reproduce a built-in component, a cute little widget, but do something completely different. So I want to talk through how this works. Fundamentally we create this class here and then follow on by defining its inner html of something called a shadow root, which I'll get to a little bit later. But I create some code here. I then grab some references to some elements within that shadow root and add some behavior. Now this behavior is pretty standard stuff. When I click on this element here, I want the other stuff to toggle state. And so I'm going to open and close this element by making it hidden or not hidden. And I'm also going to change the emoji representation, whether the element is open. So we've built this great little element. And the best part of it for me is that it has zero library cost. Obviously the element has a bit of code and that needs to run, but it costs you nothing else. There's no library to attach or download or include. And every evergreen browser, about 95% of all browsers today, supports this out of the box. This is something you can just use. The next best part is this is just a standard way of interoperability. Turns out my details, if we do a little bit more work to make it a bit more polished, will be fundamentally indistinguishable from the regular details element. Nothing you use should really care that it's my details versus details. Of course it has different functionality, but it looks, smells, tastes, quacks, whatever metaphor you like, exactly the same as anything else. So these elements are just like html. And the reason they're just like html is because we have access to these two basic components of web components. Firstly, we have access to the element lifecycle. We know when an element is being created. That's in the constructor. We know when it's being added to the page and removed from the page. The other thing, too, is that to be a custom element, you must be named with a dash in it. And that's a very simple rule. There's no built-in elements that have a dash. So your elements must have a dash. The most fun part of this, of course, is that emoji are valid strings. So it turns out your element can be called fire-firetruck-emoji if you like. The second part of web components is the part that lets you encapsulate custom html and styles. I've mentioned this in the example before to create a sort of structure that our other elements fit within. This is called Shadow DOM. Shadow DOM confusingly fits within the Shadow root. So these terms are kind of interchangeable. They mean slightly different things. But if you ever hear someone talk about Shadow DOM or Shadow root, they're basically talking about the same thing. One thing I want to talk about before we go any further is kind of this philosophical question of the web. One question we ask ourselves is do you think html is actually a very good way to build things for the web? That sounds a bit counterintuitive, right? Like we've got html and that's how you build websites. But many frameworks choose to use html as a render target. They output to divs or other elements that maybe don't have a lot of semantic meaning, but they're based on some more complicated concepts that you're building in some other framework library or some component library somewhere else. And that's totally valid. But there's an argument to make that the web can be semantic. I'll get to that later. Another concern is that you can hold web components any way you like. And what I mean by that is I'm not here coming from the position of a framework author or a library saying this is the way we think you should build things. We have some guidance, of course. We've got some suggestions of how you should use the web component life cycle to productively build elements. But in the end, you know, I'm a talk, not a cop. You can build things the way you want and you can hold it badly or really well or in a way that works for your application and not someone else's. And that's totally fine. Some interesting examples of this are we previously had an element called Santa app. Now that kind of does what it says on the tin. It was our whole Santa tracker application. So if you loaded the website, you would literally see one element called Santa app. Now it turns out we found this to be a bit of an anti-pattern. We find that web components work best when they're kind of compartmentalized things that might be reused or discrete elements that represent a single unit of work that can be tested and kind of used very easily. We found that just writing regular javascript to orchestrate a few elements was much better than writing one monolithic element. Secondly, one thing I want to call out, we often use html because it's semantic. We use header and section because that has meaning to us as developers and to people looking at our pages using screen readers. Now I can create an element called fancy link. But maybe I shouldn't because it turns out browsers don't understand that. They understand A to mean link. You can work around this. You can construct your element with A elements. But it's worth considering and keeping in mind before you just dive in and want to replace everything that html gives you for free. So I'd like to use a concrete example of divs versus semantic elements. Now, using divs like this isn't really ideal, although I suspect Gmail is targeting these divs rather than building them by hand. The class names there are pretty gross. They do have semantic meaning here. There is a role equals main hidden within this big massive layout. But to me, if I was writing a Gmail equivalent site, I think maybe I would use things like headers, nabs, some built-in elements along with a bunch of custom elements that really provide some semantic meaning. We've got this search icon here. We've got this compose button and then individual messages. To me, as a developer, this seems like a much nicer way to build this site. This is obviously up to you, but this is one of the ways you can use web components. You might imagine building a site like this. Fundamentally, though, web components are here now. You see them all over the place. You probably have used them without even realizing. For Santa Tracker, our big use case here is to drive the shell and the kind of layout of the site. We've actually got a lot of old code, and those games live within iframes and often 10-plus-year-old code that's built with jQuery, and they're a whole mess. It's one of the powers of the web. I don't have to change that code dramatically, but we do have to support it. But all the new stuff is all web components, which has made the developing for the site really quite a delight. We also see some really interesting use cases out there on the web. GitHub, somewhat semi-famously, shipped dates down to the client as 2021-05-5. What they then do at runtime is they have a Web Component which turns that absolute date into a relative date. This is actually really cool because you can actually cache that page almost forever, and then at runtime, the Web Component is going to replace that absolute time with a relative time. So that actually has a caching benefit as well. On a personal note, the way I use web components is my blog has content and headings and paragraph tags just like regular html that has been around for yonks, but I wrap up interesting demos in web components. For me, this is really nice because I don't have to create these weird layouts or structure that it has to fit inside. I literally just dump this one element on my page. In this case, it's the load demo WC, and that wraps up all the behavior and all the html and all the javascript required to run this demo. And that's really nice. So I mentioned the two parts of web components that I'm going to cover. One of them is custom elements, which is really about code loading when it sees a certain element, and Shadow DOM, which is about interesting layouts for your elements. What I'm going to do is talk through this button, the Elf Maker button, which is really a button that loads a particular game on Santa Tracker. So this is actually a really hard problem, right? If I have this button and I call it game start, I actually don't know when it gets added to the page. Now, in this case, I'm sitting in html. I could probably call an upgrade method or use a mutation observer with a bunch of flags. But by the time I do all of that, it's kind of awkward, and I've kind of written a polyfill for the feature I'm about to mention. So if I give this a dash, and as I mentioned before, anything with a dash in it can be a Web Component, the browser will basically say, cool, I've got a Santa card. I know that it's run by this class over here. It doesn't matter how it gets added, whether it's through html, another framework, being on your page that you ship to your clients, those two things will be connected up. That's really the power of custom elements. What this looks like in terms of classes is, you know, a fairly boring class. This extends html element and has two major callbacks. It's got connected callback and disconnected callback. If you've written any other code with other frameworks, this metaphor is not really new, this matches up with mount or unmount or connected or these kind of verbs that you've heard quite a lot. Lastly, what I have to do is this element doesn't for free show up on my page. I have to call this global custom elements.define that really ties the name to the class itself. So why are these methods important at all? Turns out it's about being a good web citizen and cleaning up after yourself. In this case, I'm using an animation library to play my animation as part of this card. And if I don't stop playing when I'm disconnected, then maybe I'm going to leak timeouts or animation frames as the animation tries to keep working. And this is the last time you get told about an element being disappeared from the page. So it's the right time to clean up after yourself. I want to talk about some more nuances of the life cycle. You can actually add Santa card to your page before the code arrives. That's totally fine. You can do this in either order. There's some tricky nuances about styling it before the code arrives. At that point, it's just a blank element, doesn't do anything. So there's some options there. One thing I want to push is that the life cycle that we talked about, connected, disconnected, we think is really important. Now, O components are a standard. You can use them how you like, like I mentioned. But we really think that doing setup work and tear down work on connected and disconnected makes a lot of sense. Once an element's on the page, it starts being active and should start doing things in a way that user would expect. Finally, I also want to call out two methods, attribute change callback and the static getter for observed attributes. Now, these together give you callbacks when certain attributes change, which you can do with things like mutation observer, but it ties up a lot more nicely here as part of this whole api. Next, I want to talk about shadow DOM, which can make your elements really pop. Now, obviously, this card here isn't just a standard card with text in it. Although the lovely thing about working with it on your page is that you kind of just treat it like that. It does have the shadow root, which I'll get to in a second. And I will call out that shadow DOM and the shadow root is most commonly seen on custom elements and more components. It's not the only place it can work, but it's 99% of the time the only thing you'll see here. Now, once we talk about the shadow root open, which is something you can do using dev tools or whatever, you can actually see that we've created it using a bunch of other elements. So in this case, the Santa card is made up of a real button, as well as an svg that has this pretty animation in it. What's really exciting here is that I create a style tag and in the style tag, I style every button to be blue. But importantly, it's only every button that's within the shadow root. This doesn't leak out, right? Like every page, every button on my page doesn't suddenly become blue. That would be really awkward. Now this is really a powerful feature. Now the most amazing part of shadow DOM for me is the slot element. Now fundamentally what the slot element is doing is taking direct child modes or elements of your real element Santa card in this case, and placing them somewhere within the structure of your shadow root. Now this creates a lot of interesting opportunities for layout and rearrangement of your elements. And in many ways, it's like a mini api, you know, I've got the element that basically says anything you put here will actually appear somewhere else and be styled potentially in a certain way or be phrased in a certain, you know, arrangement. The way we do this is we actually do it in the constructor of your element. And we do it in the constructor and not in connected callback because it's something that only affects the element directly. We can do it safely here because it doesn't really matter when it's added to the page. We set it up once and it's ready to go. One thing people find quite awkward about this is you actually have to write your html and css directly in line with your javascript. Now there's ways around this and people use things like template tags to make this happen more quickly or as part of your html that you ship to your clients. And that's totally fine. But if you're writing a brand new web component from scratch, you'll typically write code like this. Now some nuances of the shadow DOM that it's actually kind of gross to polyfill and we used to but in 2021 with 95% of browsers supporting it, we don't really need to. Some frameworks still provide this functionality and actually Lit does and I'll mention that in a second but it does tend to lock you in. Lit works a certain way, frameworks work a certain way and so they'll take their interpretation and layer their polyfills on top of that which doesn't really speak to the ethos of web components for me. Slots can also be named and have default content if there's nothing in them. That sort of makes sense. And the css rules about the shadow DOM and shadow scoping take a bit of getting used to. While they are scoped and nothing kind of breaks into them, you know, they don't leak in data from the outside, things like fonts do. And this makes sense from an accessibility point of view. If the user set a really large font as part of their user agent style sheet, then your elements might end up looking really big or in different ways you don't expect. And this is something that I think people potentially don't expect when they're first writing shadow DOM. There's also a few features that are sort of tied to shadow DOM but not strictly related. There's a css variable spec which lets you specify another kind of api that can change your shadow elements as well as things like part and slotted which lets you change both the targeted parts within your shadow DOM as well as the directly slotted elements under your shadow root. And so we use slotted, for example, to change the text to the lobster font in the example before. It's a sort of mini api to specify, well, what are the direct descendants of my child that I'm rearranging end up looking like? So now I want to talk about lit element which is Google's opinionated way to write web components. It's called lit now. It was formerly called lit element. And it's a descendant of something called Polymer which you might have heard of and Google used to run conferences on. You can look that up. It's fairly small. It's got a 5 kilobyte GZIP footprint. And you can find out more at lit.dev. So the main reason you use lit over regular web components is data binding. We have this name attribute here. And it's going to render as hello name or in this case, hello world. Now lit is obviously very powerful. It's much more than just data binding. But it's the main difference you'll see when you first get started. Now the code for this is pretty simple. This is what it looks like in javascript, although I will skip forward to typescript. The main difference here is that typescript supports these decorators right out of the box. So I want to talk through some of the ways you write an element and some of these features. So firstly, we don't have to call custom elements.define. Lit has a decorator for that which says this is a simple greeting. Make this class attached to that element name. Next, instead of writing styles in line with our html statement, we actually return an object. This provides a bunch of performance benefits and reuse because every element has the same styles. We also have properties. And this handles a lot to do with the data binding. So whenever we have this property, we know that when we change name, it'll cause render to be recalled whenever we change that property. Finally, we've got render itself, which returns this html tag template literal. Now this means it's quite fast. This is out of scope of the talk, but tag template literals are useful for caches. So what Lit's actually doing here is it pregenerates the html and then it will slot in the name whenever it changes. So it means it's quite fast to re-render. So as you might expect, when I change the name field to JS Nation, it now says, hello JS Nation. Works as intended. The template syntax is kind of interesting. As you saw before, we render things by saying html, backtick, then a bunch of html with some interspersed variables. But we also can specify some special kind of attributes. Now if I put a question mark before the attribute, this is a Boolean property. So it lets you toggle things like disabled attributes. If you specify a dot, this means a property. Now properties versus attributes is a whole other thing that I don't want to get into in this talk. But fundamentally, remember that if you're trying to pass through complicated objects, attributes don't make sense. Attributes can only be strings. So if you have an array or an object and you want to pass it to an element, you have to go dot property equals that property value. Next we have events. Now events are kind of interesting to me because I can just say, you know, at click for the click event, call this method. But it turns out I can just write an inline handler as well. This isn't special, right? Like, it isn't interpreting the code and saying that's the property to call. It can just call code that you pass inline. And that's kind of cool to me. Now I want to give a slightly longer example. This is rendering a list of emoji. So what I can do here is I can actually render an array of other html elements. And so in this case, I'm rendering a list of these three emoji here. And that just works, right? I return an array and it will just make it happen. Secondly, I want to show examples of how these properties and events work. So the property here on the left is passing to another web component called add widget. And that's saying maybe don't show these elements, you know, it already got these emoji. And finally, when a user hits enter, I'll get a callback on the on add method. Some other parts of Lit that I want to call out are it's got these powerful things called directives. Repeat lets you do better lists that don't cause Lit to re-render every time. Until is also a good directive that lets you do things like show spinners until a promise is resolved, which makes sense for a lot of, you know, things that talk to the internet or some database. And directives are a way to really write powerful overrides to Lit's default behavior. There's this new feature called reactive controllers, which is sort of similar, but a bit different. We've talked a lot about the web component lifecycle, but actually reactive controllers let you tie to this lifecycle without being a real element. So that's something you can read about if you are curious. Lit also now has a concept of state versus properties. Previously, it was all sort of mixed up together, but state is really useful if you have some database changes that you don't want to expose on the element, which should still cause data binding updates. One thing I find really interesting is that Lit has been taken and run with by a lot of companies to write design systems. Now design systems are, you know, whole suites of elements that you can just drop into your pages and get a certain look and feel, as well as a bunch of amazing new behavior. And so there's a whole bunch of these. You can look up any one of these pages and see a whole interpretation of, you know, a certain design system that you can drop into your pages that's built entirely with Lit element and web components. And we think that's really cool. These could be buttons, sliders, toggles, even whole site layouts that you can just drop in and use. There's also some resources here if you are curious to find out more about Lit. The website's amazing and it could do far more than I can give you in a talk. Lastly, and perhaps most importantly, I want to talk a bit about Santa Tracker and how we use web components. Now as I mentioned before, I'm the lead on Santa Tracker and I have been for many years. Santa Tracker works in a bunch of places on phones, tablets, even on TVs if we're lucky. It's a single page application. It uses web components and the goal for us really is to showcase new technologies, including web components, as well as supporting a lot of legacy code. The games aren't broken and we shouldn't throw them away and they're still quite fun. So some of those older games, they've been around for yonks and they'll hopefully stick around for a long time as well. Here's a quick demo. I want to show how the site works. You can't see where Santa is right now because it's not December. If it is December, perhaps go to your other tab and check out where he is. But we can click on some games here and have a bit of a play. We try to focus on educational games and we see this in tweets and photos we often see during the holiday season. We find a lot of classrooms do a lot of wind down time and they want to play little games. A lot of our games tend to be educational and this is something we're really pushed on. The games are things like Blockly coding games and similar that are quite fun in classrooms and are quite educational. We use about 40 web components. Now the vast majority do inherit from LitElement but a small number don't. We find that the ones that don't tend to be ones where data binding just isn't important. We often find that it's more succinct to write vanilla elements to do very complicated things like route animation libraries or interact weirdly with parts of the DOM you don't normally touch. Things like iframes and resize observers and things like that. But what I want to get down to is mix and match is actually intended, right? It's totally part of the platform to interrupt these elements in this way. Lit doesn't care that the element it's talking to is provided by web components. Lit elements don't care that other Lit elements are provided by Lit. So in many ways we've got this great interrupt layer which makes these elements work well together regardless of how you write them. Let's talk through some showcase. I've mentioned Santa Card before. One of the cool parts of this in production is that we can lock these cards to a certain date. They subscribe to a global countdown which when the date takes over they magically unfreeze and open the game. We've also got this showcase of elements from my favorite game, the Elf Maker. So this is a bunch of choosers, right? You want to choose different parts of your elf to change or display or change the color of. What's really interesting here is that the choice element at the bottom, it doesn't really deal with data binding. So it mostly deals with complicated mouse movements and resizing and making sure these elements show up properly. So in this case we have an inheritance from Lit. It doesn't make sense for us. Next I want to talk about Santa Chrome. Now Santa Chrome is a big element that really drives the whole page. It's not the Santa app. It doesn't do everything. But it's going to control the sidebar as well as the mute button and Santa's countdown to when it takes off which is obviously very important. What I find super interesting is that the nav doesn't really know that it's the sidebar. The Chrome element has a slot for the sidebar and when we place it there, whatever is in that slot will be shown when I press that button. It's a toggle. So we can test the navigation area or build it totally separately to whether I press that button to show it on the page. Lastly I want to call out we do have some awkward elements too. We have this cute little element which shows an adult reindeer trying to wrangle a little reindeer who's accidentally flying away. It's got a silly name. It's called Reindeer Wrangler. It doesn't really fit with anything else. So I want to leave you with some thoughts on web components. Why do we build web components? In the end, like I mentioned, you get to create first class citizens. We think this is really powerful. And your options are wide and varied. I've given you examples of creating small encapsulated widgets, things like my details or maybe you want to bring back the blink tag or the marquee tag. You can do that with web components and that's fine. You can also go really big and create things like Santa Chrome or pull in whole design systems which we think is really powerful. web components are the interop layer, the interop layer with others. But frameworks are an interesting beast here. So other frameworks work differently. Lit, of course, only creates web components. So we can create web components and we've seen some really cool demos of things like react and vue with only like six or seven lines of code turning their code into web components that can be shared with others. This isn't actually that common. What's more common is can your framework use web components? And the answer is typically yes because, yet again, web components just look like html elements. If your framework can work with html elements and there's not too many overrides for some special cases, then it turns out they don't care whether it's details or my details. My colleague Rob Dodson has this great article or this great website called Custom Elements Everywhere, which goes through a bunch of these use cases and shows off how different frameworks rank and rate on this kind of scale. web components work almost everywhere. This is 95% plus of browsers and increasing. This basically represents all modern evergreens and is roughly equal to the set of browsers that support script type equals module, which for me personally has been such a great high watermark. I don't even bother shipping non-module code to browsers or if I do, I'm going to send those users to a no or a low JS experience where lots of other features aren't going to work either. So I think this is really important to remember. This is a feature you can just use. web components similarly have a zero kilobyte runtime. This is not completely a fair statement. I have given you a few examples of how web components work, but I honestly think using a framework like Lit makes sense for most people when they're getting started. This is a very similar analogy to Service Workers, which we shipped a couple of years ago and is supported by all major browsers in a similar way. You can be very productive if you're a seasoned dev writing your own Service Worker from scratch, but for the vast majority of people, you're going to want to start with something like Workbox. And that's fine, right? It's not a concession. I think it's a great way to learn and to evolve and Lit falls into that same category. I think it's a great way to start writing web components without getting too knee deep in the weeds of the nuances of all the boilerplate you're going to need to write an effective component on your own. So that's it. I'm done. What I want you to take away from this talk is web components are a great thing. They're here now. I want you to go away and build some web components with Lit. It's five kilobytes on your page. It's not a huge amount of burden. And if you don't want to add it to your page, you can even just go to their Playground and poke around and build some elements right there and there. It's an amazing website that really helps you get started. Finally, go make some elves dance if you haven't already. Santa's Rock is a great place to spend some time and to have some delight with your day. Thanks for listening. I'll see you next time.
28 min
11 Jun, 2021

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

Workshops on related topic