30 Days of JavaScript: Web Components and Custom Elements, Day 16

30 Days of JavaScript: Building a Simple Weather App
30 Days of JavaScript: Web Components and Custom Elements, Day 16

Dive into Web Components and Custom Elements in JavaScript. Learn to encapsulate styles, use the Shadow DOM, and follow best practices for reusable components.

You’ve probably copied the same dropdown, the same modal, the same star-rating widget into three projects, then watched each copy drift as you patched one and forgot the others. Day 16 of our 30 Days of JavaScript series is about the browser’s own answer to that: Web Components. You define an element once, ship its markup and styles inside it, and reuse it anywhere without a framework.

Three browser features do the work: custom elements, the Shadow DOM for scoped styling, and the <template> tag for reusable markup. We’ll build each one, and I’ll flag the two places the spec bites back.

Table of Contents

Understanding Web Components

A web component is a custom HTML tag with behavior and styles attached, and it works with plain browser APIs. No build step, no dependency. That’s the appeal: the component you write today keeps working when your framework of choice falls out of fashion.

  • Custom Elements: register your own tags with their own behavior.
  • Shadow DOM: keep a component’s structure and CSS from leaking in or out.
  • HTML Templates: declare inert markup with <template> and stamp copies of it out on demand.

Creating Custom Elements

Defining a Custom Element

You write a class that extends HTMLElement, then hand it to customElements.define() with a tag name. The name must contain a hyphen, which is how the browser tells your tags apart from ones it might add later.

JS
/**
 * A simple custom element that displays a message.
 *
 * @extends {HTMLElement}
 * @returns {void}
 */
class MyElement extends HTMLElement {
  constructor() {
    super();
  }
  connectedCallback() {
    this.innerHTML = '<p>Hello, World!</p>';
  }
}
customElements.define('my-element', MyElement);

Drop <my-element></my-element> into your HTML and the browser runs your class. Note that we set the markup in connectedCallback, not the constructor. The spec forbids touching attributes or children in the constructor, so connectedCallback (which fires when the element lands in the DOM) is the right place for setup.

Extending Existing Elements

You can also subclass a built-in element instead of starting from scratch. Extend HTMLImageElement, then pass { extends: 'img' } when you register it:

JS
/**
 * Extends the built-in img element to create a custom image element.
 *
 * @extends {HTMLImageElement}
 * @returns {void}
 */
class SpecialImage extends HTMLImageElement {
  constructor() {
    super();
  }
}
customElements.define('special-img', SpecialImage, { extends: 'img' });

One catch worth knowing before you lean on this: you use it as <img is="special-img">, not as its own tag, and Safari has never shipped these “customized built-in” elements and has said it won’t. If you need Safari support, reach for an autonomous element like the one above, or a polyfill.

Shadow DOM and Styling

What is the Shadow DOM?

The Shadow DOM gives a component its own private tree. Markup and CSS inside it are sealed off: your page’s stylesheet can’t reach in, and the component’s styles can’t leak out onto the page. That’s the whole reason a third-party widget can’t wreck your layout.

Using the Shadow DOM

Call attachShadow() and set mode: 'open' so the tree is reachable later through the element’s shadowRoot property:

JS
/**
 * A custom element that uses the Shadow DOM.
 *
 * @extends {HTMLElement}
 * @returns {void}
 */
class ShadowComponent extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = '<h1>Inside Shadow DOM!</h1>';
  }
}
customElements.define('shadow-component', ShadowComponent);

Everything under that shadow root is isolated. It won’t affect the outside DOM, and the outside DOM won’t affect it.

Styling the Shadow DOM

Styles you add inside the shadow root stay inside it. Create a <style> node and append it to the shadow root:

JS
/**
 * Adds custom styles inside the Shadow DOM.
 *
 * @param {ShadowRoot} shadow - The shadow root to attach styles to.
 * @returns {void}
 */
const style = document.createElement('style');
style.textContent = 'h1 { color: red; }';
shadow.appendChild(style);

That h1 { color: red; } rule paints only the heading inside this component. An h1 elsewhere on the page stays untouched.

Advanced Topics

Lifecycle Hooks

Custom elements give you callbacks that fire at specific points in an element’s life, so you can set up and tear down cleanly:

  • connectedCallback: runs each time the element is inserted into the DOM.
  • disconnectedCallback: runs when the element is removed. Use it to drop event listeners and timers so you don’t leak memory.
  • attributeChangedCallback: runs when an attribute changes, but only for attributes you list in a static observedAttributes array. Leave that array out and this callback never fires. That trips up almost everyone the first time.
Best Practices

A few habits that keep components maintainable:

  • Keep structure and styles inside the Shadow DOM so nothing leaks either direction.
  • Reflect state through attributes and lean on the lifecycle callbacks rather than reaching into the DOM from outside.
  • Give the component real semantics: use native elements where they fit, and add ARIA roles and labels where they don’t.
Conclusion

That’s Web Components: define a tag with customElements.define(), wall off its markup and CSS with the Shadow DOM, and hook into its lifecycle with the callbacks. The payoff is a component you can drop into any project, framework or not, and trust to behave the same way every time.

The two traps to remember: do your setup in connectedCallback, not the constructor, and declare observedAttributes or attributeChangedCallback stays silent. Get those right and the rest is straightforward.

What’s Next?

Tomorrow, Day 17, we build a small weather app: fetch live data from an API and render it on the page. Bring the fetch and DOM skills from earlier in the series and we’ll wire them together into something you can actually use.

Next: 30 Days of JavaScript: Building a Simple Weather App, Day 17

Leave a Comment

Your email address will not be published. Required fields are marked *


Scroll to Top