Introduction to VanillaJS in the Age of WebComponents

WebComponents are a standard for reusable, browser-native HTML components. Thanks to this, we can now build component-based pages using native JavaScript — VanillaJS — without relying on frameworks like Angular, React, or Vue.

Before: Without Components

Imagine a website that has two HTML files — a landing page and a main content page.

In this structure, common parts like the header and footer are manually written in each file. Any update requires editing both files, which is tedious and error-prone.

After: Reusable with Components

By turning the common parts into WebComponents, we can avoid duplication and make updates in just one place.

※ index.css is identical to Plain/index.css

Further Modularization

We've now realized there are other reusable UI parts within this very page.

These are also defined as WebComponents.

You can write the following in your HTML:



and it will display the contents of the component-defined file as shown below:

(SAT/SAT.js is available at https://github.com/Satachito/SAT)

Using ShadowRoot

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 that's attached directly to 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.

Styling with adoptedStyleSheets

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.

Using for Content Projection

The element is used in WebComponents to project external content into the component's shadow DOM. It works like a placeholder that gets filled with the content placed between the opening and closing tags of the custom element.

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.

Reusing Templates

If your component's layout is defined in HTML and shared across multiple components or instances, using a is a clean solution.

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

Customized built-in elements are a type of Web Component that allow you to extend native HTML elements such as , , or . Instead of creating entirely new tags (like , customized built-in elements enhance the behavior or appearance of existing elements while preserving their built-in semantics, behaviors, and accessibility.

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., ``), where you often have to copy or forward attributes like `disabled`, `type`, or `value` yourself.

> ⚠️ 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 , you typically need to manually copy relevant attributes and properties—such as disabled—to ensure the component behaves like the original element.

Passing Data with Attributes

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.

Synchronizing Property and Attribute

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.

Open in a new tab.

Summary & Next Steps

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:

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!