Up to this point, the WebComponents we've defined haven't used shadowRoot.
The ShadowRoot creates a scoped DOM tree within a component, most notably isolating its CSS styles from the rest of the document.
As an example, let's create a simple spinner component.
If you define the @keyframes spin animation in a this (i.e., the outer component), the animation becomes globally available and may conflict with other parts of the page.
In contrast, placing the styles inside the shadowRoot ensures that the animation is scoped and won't leak out.
Also, the :host selector is a special one that targets the custom element itself from within its Shadow DOM.
For even cleaner and more performant styling within shadowRoot, you can use adoptedStyleSheets.
This allows you to define and reuse styles via JavaScript using the CSSStyleSheet API.
Unlike writing raw CSS strings inside innerHTML, adoptedStyleSheets is more efficient and lets you share styles across multiple components or even across pages.
Note: adoptedStyleSheets is supported in most modern browsers including Chrome and Safari, but older ones may require polyfills.
The
For example, see adoptedStyleSheets.html above.
You can even name slots using name attributes and assign content to them using slot attributes.
This makes it easy to build components with multiple customizable areas.
If your component's layout is defined in HTML and shared across multiple components or instances, using a
You can define a template in your HTML or in JavaScript, clone its content, and attach it to the shadow root or DOM as needed.
Customized built-in elements are a type of Web Component that allow you to extend native HTML elements such as
One major benefit of customized built-in elements is that they inherit all native behaviors and properties automatically. This means you don't need to manually proxy or reimplement attributes, events, or accessibility features — unlike with autonomous custom elements (e.g., `
> ⚠️ Note: Despite their advantages, customized built-in elements are still not widely supported across all browsers (notably Safari). For this reason, many developers still rely on autonomous custom elements for broader compatibility.
When customized built-in elements are not supported, such as in Safari, and you want to create a component that wraps a native element like
WebComponents can accept data from the outside via attributes, just like standard HTML elements.
To react to changes in attributes, define a static method called observedAttributes and implement attributeChangedCallback.
This makes it possible to make your components dynamic and responsive to attribute changes.
WebComponents receive data through attributes, but internally you'll often work with properties. They seem similar but behave differently.
It's often a good idea to keep them in sync — when an attribute is updated, reflect it to a property, and vice versa.
In this tutorial, we've learned how to build reusable and modular components using native WebComponents. By using Shadow DOM for encapsulation and adoptedStyleSheets for scoped styling, you can create clean, maintainable, and performant components with pure VanillaJS.
WebComponents allow you to build components without relying on frameworks like React, Vue, or Angular. You can leverage modern JavaScript features such as ES modules, template literals, and custom events to build robust applications.
In the future, you can extend your knowledge by:
observedAttributes and attributeChangedCallback for handling dynamic attributes.Webpack, Vite, or Rollup).We hope this guide has helped you get started with WebComponents. Feel free to explore and experiment with what you've learned today, and continue building amazing components for the web!
Happy coding!