It all looked nice in theory, but one thing shadow DOM makes worse is A11y, because element ids are now scoped and all the missing describe-by, label-for for things that should link to other side of the fence are a massive pain in the ass.
Big part of it is just skill issue on our part of course.
I am currently working on a web components framework, and I scrapped everything halfway through after I realized that you very rarely want that much encapsulation. Now, you can turn it on if you really need it, and I even made a way where you can pass references to DOM elements and stylesheets and such into each component so you can pierce the shadow veil easily. I have one demo to show it off, but I'm really having trouble imagining when someone would actually want to use it. The only time I can imagine it being useful would be building component libraries inside of large organizations.
I'm not ready to show off my framework yet, but I'm very certain that this leads to a better DX.
The trick is that the components really do need to be self contained, and you need to use slots and custom attributes to bridge the gaps. Styling is the most annoying part for me, but I just include the same global imports that the main page has.
Does this actually happen a lot? Allowing for the fact that people would rarely admit to it just being about resume padding, I feel like just wanting to _use_ the thing is a far more common motivation for poorly rationalised technology choices.
https://github.com/gitaarik/lit-state
I've used it extensively myself, for creating complex web apps with many (nested) components interacting with each other.
I don't understand why Lit hasn't gained more popularity, because for me it is basically React, but then more browser-native, much less boiler plate, and much faster rendering. There are some things you have to get used to, but when you do it doesn't limit you in any way.
I made this alternative implementation of lit-html to use as a research bed a long time ago when I was actively contributing to lit: https://github.com/ruphin/lite-html
Judging from this thread, many people have their own implementations, which is great to hear. I think there's a lot of value in solutions that are so simple that anyone can reproduce them.
Not sure why Lit showed up on the front page tonight :)
I use it in almost all my personal websites. And when I don't use it, I end up reinventing half of it and realize I should have used it from the start. This command is in most of my projects:
curl -L https://cdn.jsdelivr.net/npm/lit-html@3/lit-html.js -o ${project}/lit-html.js
I've never felt I'm using a framework or anything that deviates from Vanilla JS and valid HTML, which is why using it hardly causes any more cognitive load than using regular string templates and JavaScript functions. Which is something that I can't say about other frontend tools.Another thing I like from Lit is that with the CDN bundle, it's straightforward to experiment and use all the features without needing a build step.
Yes, Lit uses shadow DOM by default (for good reasons, I think!) and yes you can turn it off component-by-component, but that does bring some challenges.
Shadow DOM is most fundamentally just a private branch of the DOM tree for a component's internal DOM details.
Frameworks have this implicitly with DOM defined in a component's template vs passed as children. But that distinction isn't visible to other code, the browser, and CSS. This is both good and bad.
The biggest thing this separate tree gives us is the ability to tell what nodes are "children" - those are the light DOM children. Once you can separate children from internals, you can build slots. Slots are holes in the internals that children get rendered into.
Without something like shadow DOM you can't have slots. And without slots you don't have interoperable composition and you don't have viable container elements. You need some way to place children in an element that isn't confused with the element's own DOM.
So to me, before encapsulation and style scoping, interoperable composition is the most important feature, and that's really why Lit defaults to shadow DOM on. Without it we'd need some special `.children` property and Lit's component composition suddenly wouldn't be compatible with everyone else.
But the style encapsulation is often a major pain for developers, especially if they're trying to integrate web components into an existing system with whole-page stylesheets. It's a big blocker to a lot of design systems porting to web components.
That's one reason I proposed something called "Open Styleable Shadow Roots"[1] which would let styles from outer scopes cascade into a shadow root - a way to break open style encapsulation but keep slots. It's been hard convincing browser vendors that this is needed, but I'm holding out hope that it makes progress soon.
I'm sold and build all my work and personal apps with it and have for many years. I wrote this article about why in 2022:
Getting Started with Web Components & Lit
https://medium.com/gitconnected/getting-started-with-web-com...
Sometimes, I am wondering why it is not more widely used ...
Web components are nice because they're browser-native, but they don't support reactivity, which in hindisight, is a massive oversight, issue, whatever you want to call it - it's hindered adoption.
Lit is nice because there's a very straightforward progression from web components.
There is a proposal in TC39 for native signals, which I think would make a huge dent towards library-less reactivity.
I'm also working on a proposal for native reactive templating which would more-or-less obsolete lit-html. I wrote about the idea some on my blog:
- The time is right for a DOM templating API https://justinfagnani.com/2025/06/26/the-time-is-right-for-a...
- What should a native DOM templating API look like? https://justinfagnani.com/2025/06/30/what-should-a-dom-templ...
Apps are well served because they have more control about how components are used: they can import the same shared styles into every component, take are to not double-register elements, etc.
But I think there are some important standards still missing that would open things up even more in the design system and standalone components side:
- Scoped custom element registries. This moves away from a single global namespace of tag names. Seems like it's about to ship in Safari. Chrome next.
- Open styleable shadow roots. Would allow page styles to flow into shadow roots. This would make building components for use with existing stylesheets easier.
- CSS Modules. Import CSS into JS. Shipping in Chrome. About to land in Firefox.
- ARIA reference target: make idref-based reference work across shadow roots
Or with opinions like this: https://dev.to/ryansolid/web-components-are-not-the-future-4...
O if you want to go down the technical rabbit hole, you can search for all the issues people have with them, e.g.: https://x.com/Rich_Harris/status/1841467510194843982
The good part of react and friends is it's just javascript and the class is imported and referenced normally, not with a weak string-binding-through-registry kind of way.
Now add types to the mix and shadow dom and it brings constant problems without any upside.
https://news.ycombinator.com/item?id=45107388
I assume someone else looked it up and liked it enough to submit it.
Can I reassign name in the example by using document.querySelector?
I can't find clear information about how re-rendering and stateful third-party components interact.
Let's say I have a stateful data table web component that I use in the template. Is it going to be re-created every time the template is re-rendered (loosing its internal state)?
Elements are kept stable as long as the template containing them is rendered.
The template docs try to get this across by saying that Lit "re-render only the parts of template that have changed." Maybe that needs more detail.
There are details here: https://github.com/lit/lit/blob/main/dev-docs/design/how-lit...
HTTP/3, import maps, and HTML preloads can make unbundled app deployment almost reasonable in some cases. You'd still miss out on minification and better compression form a bundle. Shared compression dictionaries will make this better though.
The project is Converse.js, an XMPP chat client. It's an old project that was originally created back in 2013 with Backbone.js.
I first replaced all templates with `lit-html` when I first heard about that, and then when lit-element (and now "lit") came out, I started rewriting the project to use that.
This app has since been integrated into many different websites that rely on other frameworks like React and the fact that Converse.js is a web component (<converse-root />) makes this easier.
If you're interested, here's the Github repo: https://github.com/conversejs/converse.js And you can demo it here: https://chat.conversejs.org/
You'll need an XMPP account (see https://providers.xmpp.net/ for possible providers).
Part of using web components, for me, is that it is just javascript. There is no upgrades or deprecations to think about. Of course those things still exist on the server though, but it is easier to maintain it there.
Native web component APIs don't have the DX that many people expect though, because they are so low-level. Lit provides just that declarative reactivity on top.
In simple scenarios like just dropping it in an html page, codepen, or something like that I really enjoyed it though.
I have actually wrote a few web components by hand in an environment where I didn't want any external dependencies and when that requirement was dropped I really liked how easy was to convert them to LitElement (and how much nicer it is to work with them).
I also have embraced the shadow DOM which is a default, but I think it's more trouble than it's worth. Now I use LitElement without shadow DOM and it works great as well.
In comparison, Angular is a monster, and React is designed for the old browser capabilities, and is now staying around by inertia, not by inherent quality.
Its .html temples were shipped unmodified directly to the client (yes, including comments). Except they weren't actually html, and sometimes the browser would try to clean them up, breaking the template.
Reactivity was achieved through all kinds of weird mechanisms (eg monkey-patching arrays to watch for mutations). It would frequently resort to polling on every tick or break completely.
DI used TypeScripts experimental decorators, even long after it was clear that it would never become stable.
On the other hand, templates weren't type checked.
Its .html temples were shipped unmodified directly to the client. Except they weren't actually html, and sometimes the browser would try to clean them up, breaking the template.
Reactivity was achieved through all kinds of weird mechanisms (eg monkey-patching arrays to watch for mutations). It would frequently resort to polling on every tick or break completely.
DI used TypeScripts experimental decorators, even long after it was clear that it would never become stable.
On the other hand, templates weren't type checked.
I think lit should distance itself from that mess if possible
What would be the benefit of rebuilding these components in Lit over using Vue to build them?
https://vuejs.org/guide/extras/web-components#building-custo...
* Built-in state management especially as in v3 (ref/reactive) is super powerful, and you can test the setup function logic by itself without any dependency on DOM which is convenient. By comparison, reactivity in Lit is basic (by design), and mostly work on the widget level -- if something changes, you need to trigger the update process of the widget.
* The lifecycle is also a bit simpler. In Lit, you often need to work with lifecycle callbacks like firstUpdated, connected, shouldUpdate, updated, disconnected etc. (You don't need to implement these for simpler widgets, but often you have to.) You can easily run into bugs if you are not careful, especially in the less common situation where your widget can be both temporarily and permanently removed from the DOM, in which case you need to make sure the state of the widget is properly perserved, which isn't a thing you need to worry about in Vue. We got bitten by this multiple times.
Unless there is a strong technical reason, I suggest that you focus on shipping features instead of changing your tech stack. Rebuilding your widgets is a very time consuming task with, well, near 0 impact on the user.
Vue is more of a "framework" solution and has more things built-in. You can do the same things with Lit, but the implementation would look different, because you'd lean more on native APIs. A good example of that is the event model, Vue has some event model built in, but with Lit you would use EventTarget.dispatchEvent().
Lit is a runtime solution, it doesn't require a build and you can load your source directly in the browser. Vue on the other hand requires some form of compiler stage to produce something your browser can use. Compilers these days are fast, and Lit is specifically engineered to not have runtime performance overhead, so in practice this difference is rather minor. It is a very fundamental difference, so I think it's worth pointing out.
Vue can compile to other targets. If you are only delivering Web Components, this is mostly irrelevant, but in theory a consumer might be able to use the Vue components directly in their Vue project, which might give them a better DX. On the other hand, Lit is specifically designed to produce Web Components, so you'll probably have a bit less friction compares to using Vue, e.g when some Vue concept doesn't compile cleanly to Web Components.
Is there a major benefit to choosing one implementation over the other? I don't think so, unless you have a very particular requirement that one of them addresses that the other doesn't. For nearly all cases, it is just a different implementation syntax.
In most cases the only relevant metric in deciding between them is what technology your developers are more familiar/comfortable with.
Make a fat bundle with a web component or even a mount() function exported and run whatever fits your devx inside, with the API surface as narrow as possible. As long as it's self contained and the size is reasonable, nobody on a consumer side cares how you cook, so optimize your own pipelines to build, test and publish it.
People will build adapters to fit it into their stuff anyway and will let you know.
As I recall inside Google it was maybe one in a thousand elements that needed any changes at all. I updated the entire internal codebase of many tens of thousands of elements in a couple weeks of part time work.
But more importantly Lit 2 and Lit 3 are interoperable, so there's not the same pressure to update. When an element or library updates from Lit 2 to Lit 3, it can do that as a point release, because its public API is the same. This really reduces the amount of upgrade toil you have to deal with.
Really love the abstraction that makes web components easy to use.
I shipped a project with Lit and I liked it, but I didn't like that I'd need to know the complete project scope up front that I could write everything from the ground up. I know I could use React component for some of the harder stuff but at that point might as well use React and avoid bundling two systems
- https://github.com/vaadin/web-components
- https://github.com/material-components/material-web (very, very sadly killed by google management)
I love it when I visit one of my pages and use Lighthouse to check it out and have nearly straight across 100 scores. Also, I usually have really great performance on phones as well because the pages are so light and quick to render.
Typescript can even add Intellisense to "customElements.get" by augmenting CustomElementRegistry: https://gist.github.com/cecilemuller/72fbb3bc3a77d82c8a969cd...
We use them to make fields reactive mostly, and I love how declarative they are. But we use them sparingly. I personally don't love how some libraries try to put a lot of things into decorators that could have been standard class features, like a static field or a method.
edit: As mentioned by skrebbel, decorators are optional. Every decorator has a simple plain-JS way of doing it. Event reactive properties: https://lit.dev/docs/components/properties/#declaring-proper...
We also put a lot of effort into making all of our documentation and playground samples on lit.dev available in both JavaScript and TypeScript with decorators. There's a switch that will change everything on the site from JS to TS globally.
I can think about few other ways, such as using higher order functions/classes, using getOwnPropertyDescriptor or doing stuff at construction.
> As mentioned by skrebbel, decorators are optional
This is not a pro, it's a con. The more ways there are to achieve the same result the more inconsistent projects become IRL.
Also, do you really want to metaprogram at all? What's the huge benefit of that approach?
No, they are not. Decorators don't even exist in JavaScript. Stop assuming typescript is Javascript or even worse, that everybody is on board.
> There's a switch that will change everything on the site from JS to TS globally.
Lol.
The Lit authors tried hard to use vanilla JS everywhere they could, and it shows.
export class SimpleGreeting extends LitElement {
...
}
customElements.define('simple-greeting', SimpleGreeting);My editor: https://needcoolershoes.com
If you're curious about lit and like longer form content - I recommend watching the [0] http 203 video that talks about lit element and other tools like it
Using other frameworks to set up web view’s just don’t feel the same by comparison. I just want nested web components, and I just want Lit to help me define them. Tagged template literals for constructing HTML feels so much better than suffering through JSX.
- Custom HTML-like syntax
<button @click="" .disabled="" />
- Custom Javascript rules // valid JS, invalid lit
const tagName= "a";
`<${tagName} href="">Some link</${tagName}>`
- Custom rules for special functions. // classMap looks like a regular JS function, but it's not.
// Both of these will produce an error
<div class="my-widget ${classMap(dynamicClasses)} ${classMap(dynamicClasses)}">Static and dynamic</div>
<div data-class="${classMap(dynamicClasses)}">Static and dynamic</div>
- Context https://lit.dev/docs/data/context/- Experimental compiler: https://github.com/lit/lit/tree/main/packages/labs/compiler#...
Yes, Lit templates give some special meaning to attribute names with a few prefixes. No, it's not "HTML-like". It's valid HTML. Not that it matters much. You bring this up all the time but I'm not sure what the actual criticism is. Developers seem to understand the small syntax carve-out just fine.
No, there are no custom JavaScript rules. Templates have some rules. I'm not sure why they wouldn't? In general you can't make things like tag and attribute names dynamic because you can't change them in HTML. You can actually write the template you show with what we call static templates though.
`classMap()` is a template directive. It has some rules about how it's used in templates, just like other JS functions can have rules about how their used. I'm not sure what makes that not a function.
But to your main point: Lit is not like React because it's not a framework. Lit helps you make custom elements - it's an implementation detail of some web components. Everything else about those elements: how you instantiate them, style them, where they work, etc., is all defined by the HTML and DOM standards. React is a framework, and defines its own rules about how its components work.
I love that I get to choose how everything works. I have custom systems for routing, styling, signals, API interactions. I control who gets passed what and when. It makes the app feel light, responsive, and -- most importantly, it fits in my head and I understand how it's put together because I chose it, not because I learned it from someone.
Yes, yes I do. Because Web Components are almost 15 years now, and they still struggle with the most basic of things. How's that abandoned roadmap going? https://w3c.github.io/webcomponents-cg/2022.html
> No, there are no custom JavaScript rules. Templates have some rules.
That is "here are regular JS functions. However, you cannot use them as regular JS functions in these specific contexts". Reminds me of certain very specific rules about specific functions in specific contexts in some other framework. Can't put my finger on it.
> I'm not sure what makes that not a function.
I never said it doesn't make it a function.
> But to your main point: Lit is not like React because it's not a framework.
Yeah, React wasn't a framework either, but just a library. Everything about DOM elements that React produces, how you instantiate them, style them, where they work etc. is all defined by the HTML and DOM standards.
But then React grew in the number of features, and can no longer be called a library even though still the only thing it does is output some DOM nodes.
I guess you'll insist on Lit being "just a library" even after it adds a ton of other functionality all other frameworks already have or are moving towards.
> I'm not sure what the actual criticism is
The criticism is usual: Lit is rapidly absorbing all the features from all the other frameworks and becoming a framework itself while many of its developers and proponents can't stop shitting all over other frameworks.
Typescript has JSX built in so when "I made my own framework" I just used that.
Looking at the first example:
First I had to switch it from TS to JS. As I don't consider something that needs compilation before it runs to be lightweight.
Then, the first line is:
import {html, css, LitElement} from 'lit';
What is this? This is not a valid import. At least not in the browser. Is the example something that you have to compile on the server to make it run in the browser?And when I use the "download" button on the playground version of the first example, I get a "package.json" which defines dependencies. That is also certainly not something a browser can handle.
So do I assume correctly that I need to set up a webserver, a dependency manager, and a serverside runtime to use these "light weight" components?
Or am I missing something? What would be the minimal amount of steps to save the example and actually have it run in the browser?
https://unpkg.com/lit@3.3.1/index.js?module
You can even dynamically import that in the a running browser console and use it directly on any webpage.
I guess for most people the standard is to install things from NPM
"things" that run in the browser? replace 'lit' with something like this:
https://unpkg.com/lit@3.3.1/index.js?module
Thanks, that works: