Web Components Demystified | CSS-Tricks

Scott Jehl released a course called Web Components Demystified. I love that name because it says what the course is about right on the tin: you’re going to learn about web components and clear up any confusion you may already have about them.
And there’s plenty of confusion to go around! “Components” is already a loaded term that’s come to mean everything from a piece of UI, like a search component, to an element you can drop in and reuse anywhere, such as a React component. The web is chock-full of components, tell you what.
But what we’re talking about here is a set of standards where HTML, CSS, and JavaScript rally together so that we can create custom elements that behave exactly how we want them to. It’s how we can make an element called
and the browser knows what to do with it.
This is my full set of notes from Scott’s course. I wouldn’t say they’re complete or even a direct one-to-one replacement for watching the course. You’ll still want to do that on your own, and I encourage you to because Scott is an excellent teacher who makes all of this stuff extremely accessible, even to noobs like me.
Chapter 1: What Web Components Are… and Aren’t
Web components are not built-in elements, even though that’s what they might look like at first glance. Rather, they are a set of technologies that allow us to instruct what the element is and how it behaves. Think of it the same way that “responsive web design” is not a thing but rather a set of strategies for adapting design to different web contexts. So, just as responsive web design is a set of ingredients — including media fluid grids, flexible images, and media queries — web components are a concoction involving:
Custom elements
These are HTML elements that are not built into the browser. We make them up. They include a letter and a dash.
Hey, I'm Fancy
We’ll go over these in greater detail in the next module.
HTML templates
Templates are bits of reusable markup that generate more markup. We can hide something until we make use of it.
Much more on this in the third module.
Shadow DOM
The DOM is queryable.
document.querySelector("h1");
//
The Shadow DOM is a fragment of the DOM where markup, scripts, and styles are encapsulated from other DOM elements. We’ll cover this in the fourth module, including how to
content.
There used to be a fourth “ingredient” called HTML Imports, but those have been nixed.
In short, web components might be called “components” but they aren’t really components more than technologies. In React, components sort of work like partials. It defines a snippet of HTML that you drop into your code and it outputs in the DOM. Web Components are built off of HTML Elements. They are not replaced when rendered the way they are in JavaScript component frameworks. Web components are quite literally HTML elements and have to obey HTML rules. For example:
We’re generating meaningful HTML up-front rather than rendering it in the browser through the client after the fact. Provide the markup and enhance it! Web components have been around a while now, even if it seems we’re only starting to talk about them now.
Chapter 2: Custom Elements
First off, custom elements are not built-in HTML elements. We instruct what they are and how they behave. They are named with a dash and at must contain least one letter. All of the following are valid names for custom elements:
Just remember that there are some reserved names for MathML and SVG elements, like
. Also, they cannot be void elements, e.g.
, meaning they have to have a correspoonding closing tag.
Since custom elements are not built-in elements, they are undefined by default — and being undefined can be a useful thing! That means we can use them as containers with default properties. For example, they are display: inline
by default and inherit the current font-family
, which can be useful to pass down to the contents. We can also use them as styling hooks since they can be selected in CSS. Or maybe they can be used for accessibility hints. The bottom line is that they do not require JavaScript in order to make them immediately useful.
Working with JavaScript. If there is one
on the page, we can query it and set a click handler on it with an event listener. But if we were to insert more instances on the page later, we would need to query it when it’s appended and re-run the function since it is not part of the original document rendering.
Defining a custom element
This defines and registers the custom element. It teaches the browser that this is an instance of the Custom Elements API and extends the same class that makes other HTML elements valid HTML elements:
My Element
Check out the methods we get immediate access to:

Breaking down the syntax
customElements
.define(
"my-element",
class extends HTMLElement {}
);
// Functionally the same as:
class MyElement extends HTMLElement {}
customElements.define("my-element", MyElement);
export default myElement
// ...which makes it importable by other elements:
import MyElement from './MyElement.js';
const myElement = new MyElement();
document.body.appendChild(myElement);
//
//
//
// Or simply pull it into a page
// Don't need to `export default` but it doesn't hurt to leave it
// My Element
//
It’s possible to define a custom element by extending a specific HTML element. The specification documents this, but Scott is focusing on the primary way.
class WordCount extends HTMLParagraphElement
customElements.define("word-count", WordCount, { extends: "p" });
// This is a custom paragraph!
Scott says do not use this because WebKit is not going to implement it. We would have to polyfill it forever, or as long as WebKit holds out. Consider it a dead end.
A component has various moments in its “life” span: The lifecycle
- Constructed (
constructor
) - Connected (
connectedCallback
) - Adopted (
adoptedCallback
) - Attribute Changed (
attributeChangedCallback
) - Disconnected (
disconnectedCallback
)
class myElement extends HTMLElement {
constructor() {}
connectedCallback() {}
adoptedCallback() {}
attributeChangedCallback() {}
disconnectedCallback() {}
}
customElements.define("my-element", MyElement);
constructor()
class myElement extends HTMLElement {
constructor() {
// provides us with the `this` keyword
super()
// add a property
this.someProperty = "Some value goes here";
// add event listener
this.addEventListener("click", () => {});
}
}
customElements.define("my-element", MyElement);
“When the constructor is called, do this…” We don’t have to have a constructor when working with custom elements, but if we do, then we need to call super()
because we’re extending another class and we’ll get all of those properties.
Constructor is useful, but not for a lot of things. It’s useful for setting up initial state, registering default properties, adding event listeners, and even creating Shadow DOM (which Scott will get into in a later module). For example, we are unable to sniff out whether or not the custom element is in another element because we don’t know anything about its parent container yet (that’s where other lifecycle methods come into play) — we’ve merely defined it.
connectedCallback()
class myElement extends HTMLElement {
// the constructor is unnecessary in this example but doesn't hurt.
constructor() {
super()
}
// let me know when my element has been found on the page.
connectedCallback() {
console.log(`${this.nodeName} was added to the page.`);
}
}
customElements.define("my-element", MyElement);
Note that there is some strangeness when it comes to timing things. Sometimes isConnected
returns true
during the constructor. connectedCallback()
is our best way to know when the component is found on the page. This is the moment it is connected to the DOM. Use it to attach event listeners.
If the
tag comes before the DOM is parsed, then it might not recognize childNodes
. This is not an uncommon situation. But if we add type="module"
to the
, then the script is deferred and we get the child nodes. Using setTimeout
can also work, but it looks a little gross.
disconnectedCallback
class myElement extends HTMLElement {
// let me know when my element has been found on the page.
disconnectedCallback() {
console.log(`${this.nodeName} was removed from the page.`);
}
}
customElements.define("my-element", MyElement);
This is useful when the component needs to be cleaned up, perhaps like stopping an animation or preventing memory links.
adoptedCallback()
This is when the component is adopted by another document or page. Say you have some iframes on a page and move a custom element from the page into an iframe, then it would be adopted in that scenario. It would be created, then added, then removed, then adopted, then added again. That’s a full lifecycle! This callback is adopted automatically simply by picking it up and dragging it between documents in the DOM.
Unlike React, HTML attributes are strings (not props!). Global attributes work as you’d expect, though some global attributes are reflected as properties. You can make any attribute do that if you want, just be sure to use care and caution when naming because, well, we don’t want any conflicts. Custom elements and attributes

elements. We could say data-type
instead. (Remember that Chris has a comprehensive guide on using data attributes.)
Here’s a quick example showing how to get a Examples
greeting
attribute and set it on the custom element:
class MyElement extends HTMLElement {
get greeting() {
return this.getAttribute('greeting');
// return this.hasAttribute('greeting');
}
set greeting(val) {
if(val) {
this.setAttribute('greeting', val);
// this setAttribute('greeting', '');
} else {
this.removeAttribute('greeting');
}
}
}
customElements.define("my-element", MyElement);
Another example, this time showing a callback for when the attribute has changed, which prints it in the element’s contents:
hello
A few more custom element methods:
customElements.get('my-element');
// returns MyElement Class
customElements.getName(MyElement);
// returns 'my-element'
customElements.whenDefined("my-element");
// waits for custom element to be defined
const el = document.createElement("spider-man");
class SpiderMan extends HTMLElement {
constructor() {
super();
console.log("constructor!!");
}
}
customElements.define("spider-man", SpiderMan);
customElements.upgrade(el);
// returns "constructor!!"
Custom methods and events:
Bring your own base class, in the same way web components frameworks like Lit do:
class BaseElement extends HTMLElement {
$ = this.querySelector;
}
// extend the base, use its helper
class myElement extends BaseElement {
firstLi = this.$("li");
}
Practice prompt
Create a custom HTML element called
that displays the text “Hi, World!” when added to the page:
Enhance the element to accept a name
attribute, displaying "Hi, [Name]!"
instead:
Chapter 3: HTML Templates
The Templates are designed to hold HTML fragments: A template is selectable in CSS; it just doesn’t render. It’s a document fragment. The inner document is a No, not in CSS, but JavaScript. We can query the inner contents of a template and print them somewhere else. Hi Test Hi Oops, the component only works one time! We need to clone it if we want multiple instances: Hi Let’s stub out a template for a list item and then insert them into an unordered list: The other way to use templates that we’ll get to in the next module: Shadow DOM Hi, I'm in the Shadow DOM Here we go, this is a heady chapter! The Shadow DOM reminds me of playing bass in a band: it’s easy to understand but incredibly difficult to master. It’s easy to understand that there are these nodes in the DOM that are encapsulated from everything else. They’re there, we just can’t really touch them with regular CSS and JavaScript without some finagling. It’s the finagling that’s difficult to master. There are times when the Shadow DOM is going to be your best friend because it prevents outside styles and scripts from leaking in and mucking things up. Then again, you’re most certainly going go want to style or apply scripts to those nodes and you have to figure that part out. That’s where web components really shine. We get the benefits of an element that’s encapsulated from outside noise but we’re left with the responsibility of defining everything for it ourselves. We covered the This will render in the Shadow DOM. In this case, the There are times you’re going to want to “pierce” the Shadow DOM to allow for some styling and scripts. The content is relatively protected but we can open the Now we can query the This will render in the Shadow DOM. We need that When you add a shadow root, it becomes the only rendered root in that shadow host. Any elements after a shadow root node in the DOM simply don’t render. If a DOM element contains more than one shadow root node, the ones after the first just become template tags. It’s sort of like the Shadow DOM is a monster that eats the siblings. Slots bring those siblings back! I'm a sibling of a shadow root, and I am visible. All of the siblings go through the slots and are distributed that way. It’s sort of like slots allow us to open the monster’s mouth and see what’s inside. Using templates is the declarative way to define the Shadow DOM. We can also define the Shadow DOM imperatively using JavaScript. So, this is doing the exact same thing as the last code snippet, only it’s done programmatically in JavaScript: This will render in the Shadow DOM. Another example: So, is it better to be declarative or imperative? Like the weather where I live, it just depends. We can set the shadow mode via Javascript as well: About that last one, it says we have to manually insert the This WILL render in shadow DOM but not automatically. Scott spent a great deal of time sharing examples that demonstrate different sorts of things you might want to do with the Shadow DOM when working with web components. I’ll rapid-fire those in here. Back to this example: Let’s get that string out of our JavaScript with reusable imperative shadow HTML: This item is currently:
Slightly better as it grabs the component’s name programmatically to prevent name collisions: This item is currently:
Long story, cut short: maybe don’t create custom form controls as web components. We get a lot of free features and functionalities — including accessibility — with native form controls that we have to recreate from scratch if we decide to roll our own. In the case of forms, one of the oddities of encapsulation is that form submissions are not automatically connected. Let’s look at a broken form that contains a web component for a custom input: This input’s value won’t be in the submission! Also, form validation and states are not communicated in the Shadow DOM. Similar connectivity issues with accessibility, where the shadow boundary can interfere with ARIA. For example, IDs are local to the Shadow DOM. Consider how much you really need the Shadow DOM when working with forms. The moral of the last section is to tread carefully when creating your own web components for form controls. Scott suggests avoiding that altogether, but he continued to demonstrate how we could theoretically fix functional and accessibility issues using element internals. Let’s start with an input value that will be included in the form submission. Now let’s slot this imperatively: The value is not communicated yet. We’ll add a static Then we’ll set the form value as part of the internals when the input’s value changes: Here’s how we set states with element internals: Let’s toggle a “add” or “delete” a boolean state: Let’s refactor this for ARIA improvements: Phew, that’s a lot of work! And sure, this gets us a lot closer to a more functional and accessible custom form input, but there’s still a long way’s to go to achieve what we already get for free from using native form controls. Always question whether you can rely on a light DOM form instead. Styling web components comes in levels of complexity. For example, we don’t need any JavaScript at all to slap a few styles on a custom element. Hi Hi Let’s poke at it from the other direction: Hi Hi Same idea, but setting the color on the Hi Hi We can target the paragraph in the Hi Hi We can scope things to the shadow root’s Hi Hi New problem! What if the Light DOM styles are scoped to the universal selector instead? Hi Hi This breaks the custom element’s styles. But that’s because Shadow DOM styles are applied before Light DOM styles. The styles scoped to the universal selector are simply applied after the According to Scott, Hi Hi There are some useful selectors we have to look at components from the outside, looking in. We just looked at this! But note how it is a function in addition to being a pseudo-selector. It’s sort of a parent selector in the sense that we can pass in the Hi Hi This targets the shadow host but only if the provided selector is a parent node anywhere up the tree. This is super helpful for styling custom elements where the layout context might change, say, from being contained in an Defining an element occurs when it is created, and this pseudo-selector is how we can select the element in that initially-defined state. I imagine this is mostly useful for when a custom element is defined imperatively in JavaScript so that we can target the very moment that the element is constructed, and then set styles right then and there. Minor note about protecting against a flash of unstyled content (FOUC)… or unstyled element in this case. Some elements are effectively useless until JavsScript has interacted with it to generate content. For example, an empty custom element that only becomes meaningful once JavaScript runs and generates content. Here’s how we can prevent the inevitable flash that happens after the content is generated: Warning zone! It’s best for elements that are empty and not yet defined. If you’re working with a meaningful element up-front, then it’s best to style as much as you can up-front. This does not style the paragraph The Shadow DOM cannot style this content directly. The styles would apply to a paragraph in the Slots are part of the Light DOM. So, this works: This means that slots are easier to target when it comes to piercing the shadow root with styles, making them a great method of progressive style enhancement. We have another special selected, the Unfortunately, This is another place where A part is a way of offering up Shadow DOM elements to the parent document for styling. Let’s add a part to a custom element: Without the We can use this to expose specific “parts” of the custom element that are open to outside styling, which is almost like establishing a styling API with specifications for what can and can’t be styled. Just note that A bit in the weeds here, but we can export parts in the sense that we can nest elements within elements within elements, and so on. This way, we include parts within elements. We discussed this when going over element internals in the chapter about the Shadow DOM. But it’s worth revisiting that now that we’re specifically talking about styling. We have a We also have access to the Custom properties cross the Shadow DOM barrier! There’s the classic ol’ external Slotted Element It might seem like an anti-DRY approach to call the same external stylesheet at the top of all web components. To be clear, yes, it is repetitive — but only as far as writing it. Once the sheet has been downloaded once, it is available across the board without any additional requests, so we’re still technically dry in the sense of performance. CSS imports also work: Slotted Element One more way using a JavaScript-based approach. It’s probably better to make CSS work without a JavaScript dependency, but it’s still a valid option. We have a JavaScript module and import CSS into a string that is then adopted by the shadow root using Container queries are nice to pair with components, as custom elements and web components are containers and we can query them and adjust things as the container changes. In this example, we’re setting styles on the How web component features are used together! In this chapter, Scott focuses on how other people are using web components in the wild and highlights a few of the more interesting and smart patterns he’s seen. It’s often the very first example used in React tutorials. Reef is a tiny library by Chris Ferdinandi that weighs just 2.6KB minified and zipped yet still provides DOM diffing for reactive state-based UIs like React, which weighs significantly more. An example of how it works in a standalone way: This sets up a “signal” that is basically a live-update object, then calls the So, for example, we can update those values on We can combine this sort of library with a web component. Here, Scott imports Reef and constructs the data outside the component so that it’s like the application state: It’s the virtual DOM in a web component! Another approach that is more reactive in the sense that it watches for changes in attributes and then updates the application state in response which, in turn, updates the greeting. If the attribute changes, it only changes that instance. The data is registered at the time the component is constructed and we’re only changing string attributes rather than objects with properties. This describes web components that are not empty by default like this: This is a “React” mindset where all the functionality, content, and behavior comes from JavaScript. But Scott reminds us that web components are pretty useful right out of the box without JavaScript. So, “HTML web components” refers to web components that are packed with meaningful content right out of the gate and Scott points to Jeremy Keith’s 2023 article coining the term. […] we could call them “HTML web components.” If your custom element is empty, it’s not an HTML web component. But if you’re using a custom element to extend existing markup, that’s an HTML web component. Jeremy cites something Robin Rendle mused about the distinction: […] I’ve started to come around and see Web Components as filling in the blanks of what we can do with hypertext: they’re really just small, reusable chunks of code that extends the language of HTML. The “React” way: The props look like HTML but they’re not. Instead, the props provide information used to completely swap out the Web components can do that, too: Same deal, real HTML. Progressive enhancement is at the heart of an HTML web component mindset. Here’s how that web component might work: But a better starting point would be to include the This way, the image is downloaded and ready before JavaScript even loads on the page. Strive for augmentation over replacement! This helps developers test responsive component layouts, particularly ones that use container queries. This is like embedding a YouTube video, but without bringing along all the baggage that YouTube packs into a typical embed snippet. This is part of the 11ty project. It allows you to define custom elements as files, writing everything as a single file component. This is inside the element
This is Scott’s favorite! It renders web components on the server. Web components can render based on application state per request. It’s a way to use custom elements on the server side. This is a super short module simply highlighting a few of the more notable libraries for web components that are offered by third parties. Scott is quick to note that all of them are closer in spirit to a React-based approach where custom elements are more like replaced elements with very little meaningful markup to display up-front. That’s not to throw shade at the libraries, but rather to call out that there’s a cost when we require JavaScript to render meaningful content. Most components are not exactly HTML-first. The pattern is closer to replaced elements. There’s plenty of complexity, but that makes sense for a system that drives an application like Photoshop and is meant to drop into any project. But still, there is a cost when it comes to delivering meaningful content to users up-front. An all-or-nothing approach like this might be too stark for a small website project. Scott covers what the future holds for web components as far as he is aware. Define an element in HTML alone that can be used time and again with a simpler syntax. There’s a GitHub issue that explains the idea, and Zach Leatherman has a great write-up as well. Make it easier to pair custom elements with other elements in the Light DOM as well as other custom elements through ARIA. How can we use container queries without needing an extra wrapper around the custom element? This was one of the web components’ core features but was removed at some point. They can define HTML in an external place that could be used over and over. This is also known as “open styling.” This would be a templating feature that allows for JSX-string-literal-like syntax where variables inject data. And the application has produced a template with the following content: Using variations of the same web component without name collisions. element is not for users but developers. It is not exposed visibly by browsers.
The browser ignores everything in here.
#document-fragment
. Not sure why you’d do this, but it illustrates the point that templates are selectable:template { display: block; }` /* Nope */
template + div { height: 100px; width: 100px; } /* Works */
The content
property
Using a Document Fragment without a
const myFrag = document.createDocumentFragment();
myFrag.innerHTML = "
Clone a node
A more practical example
Chapter 4: Shadow DOM
Using the Shadow DOM
element in the last chapter and determined that it renders in the Shadow DOM without getting displayed on the page.
is rendered as a
#shadow-root
without the element’s tags. It’s a fragment of code. So, while the paragraph inside the template is rendered, the
itself is not. It effectively marks the Shadow DOM’s boundaries. If we were to omit the
shadowrootmode
attribute, then we simply get an unrendered template. Either way, though, the paragraph is there in the DOM and it is encapsulated from other styles and scripts on the page.
Breaching the shadow
shadowrootmode
and allow some access.div
that contains the and select the
#shadow-root
:
document.querySelector("div").shadowRoot
// #shadow-root (open)
//
is not actually rendered at all.
Additional shadow attributes
Shadow DOM siblings
Declaring the Shadow DOM
// open
this.attachShadow({mode: open});
// closed
this.attachShadow({mode: closed});
// cloneable
this.attachShadow({cloneable: true});
// delegateFocus
this.attachShadow({delegatesFocus: true});
// serialized
this.attachShadow({serializable: true});
// Manually assign an element to a slot
this.attachShadow({slotAssignment: "manual"});
elements in JavaScript:
Examples
Get an array of element nodes in a slot
this.shadowRoot.querySelector('slot')
.assignedElements();
// get an array of all nodes in a slot, text too
this.shadowRoot.querySelector('slot')
.assignedNodes();
When did a slot’s nodes change?
let slot = document.querySelector('div')
.shadowRoot.querySelector("slot");
slot.addEventListener("slotchange", (e) => {
console.log(`Slot "${slot.name}" changed`);
// > Slot "saying" changed
})
Combining imperative Shadow DOM with templates
Forms with Shadow DOM
Element internals
formAssociated
variable with internals attached:// add a checked state
this.internals.states.add("checked");
// remove a checked state
this.internals.states.delete("checked");
Chapter 5: Styling Web Components
closed
to open
doesn’t change CSS. It allows JavaScript to pierce the Shadow DOM but CSS isn’t affected.
Let’s poke at it
, even if the shadow root’s mode is set to
open
.
declarations in the
are encapsulated and do not leak out to the other paragraphs, even though it is declared later in the cascade.
:
color
. The is the parent and everything in it is a child that inherits these styles, including custom elements.
Let’s fight with inheritance
style block to override the styles set on the
. Those won’t leak back to the other paragraphs.
all: initital
as a defensive strategy against future inheritable styles. But what if we add more elements to the custom element? It’s a constant fight.
Host styles!
:host
selector to keep things protected.
:host
styles, which overrides what we have in the shadow root. So, we’re still locked in a brutal fight over inheritance and need stronger specificity.!important
is one of the only ways we have to apply brute force to protect our custom elements from outside styles leaking in. The keyword gets a bad rap — and rightfully so in the vast majority of cases — but this is a case where it works well and using it is an encouraged practice. It’s not like it has an impact on the styles outside the custom element, anyway.
Special selectors
:host()
and that becomes the scoping context for the entire selector, meaning the
!important
keyword is no longer needed.
:host-context()
versus being contained in a
.
:defined
Styling slots
green
as you might expect:
that gets rendered in the Light DOM, but it cannot style it when it is slotted into the
.
::slotted()
pseudo-element that’s also a function. We pass it an element or class and that allows us to select elements from within the shadow root.
::slotted()
is a weak selected when compared to global selectors. So, if we were to make this a little more complicated by introducing an outside inheritable style, then we’d be hosed again.
!important
could make sense. It even wins if the global style is also set to !important
. We could get more defensive and pass the universal selector to ::slotted
and set everything back to its initial value so that all slotted content is encapsulated from outside styles leaking in.
Styling :parts
part
attribute, there is no way to write styles that reach the paragraph. But with it, the part is exposed as something that can be styled.
::part
cannot be used as part of a complex selector, like a descendant selector:
Styling states and validity
:state
pseudo-function that accepts our defined states.
:invalid
pseudo-class.
Cross-barrier custom properties
Adding stylesheets to custom elements
way of going about it:
shadowRoort.adoptedStyleSheets
. And since adopted stylesheets are dynamic, we can construct one, share it across multiple instances, and update styles via the CSSOM that ripple across the board to all components that adopt it.
Container queries!
:host()
to define a new container, as well as some general styles that are protected and scoped to the shadow root. From there, we introduce a container query that updates the unordered list’s layout when the custom element is at least 50em
wide.
Next up…
Chapter 6: HTML-First Patterns
Let’s start with a typical counter
Reef
component()
method to select where we want to make the update, and it injects a template literal in there that passes in the variables with the markup we want.setTimeout
:
HTML Web Components
tag with the JavaScript-based markup.class UserAvatar extends HTMLElement {
connectedCallback() {
const src = this.getAttribute("src");
const name = this.getAttribute("name");
this.innerHTML = `
directly in the component so that the markup is immediately available:
resizeasaurus
lite-youtube-embed
It starts with a link which is a nice fallback if the video fails to load for whatever reason. When the script runs, the HTML is augmented to include the video .
Chapter 7: Web Components Frameworks Tour
Lit extends the base class and then extends what that class provides, but you’re still working directly on top of web components. There are syntax shortcuts for common patterns and a more structured approach.
The package includes all this in about 5-7KB:
Lit
Pros
Cons
Ecosystem
No official SSR story (but that is changing)
Community
Familiar ergonomics
Lightweight
Industry-proven
webc
Pros
Cons
Community
Geared toward SSG
SSG progressive enhancement
Still in early stages
Single file component syntax
Zach Leatherman!
Enhance
Pros
Cons
Ergonomics
Still in early stages
Progressive enhancement
Single file component syntax
Full-stack stateful, dynamic SSR components
Chapter 8: Web Components Libraries Tour
Spectrum
FAST
Shoelace
Chapter 9: What’s Next With Web Components
Declarative custom elements
Cross-root ARIA
Container Queries
HTML Modules
External styling
DOM Parts
Scoped element registries