Web Components and Custom Elements
Web Components are a set of browser-native standards — Custom Elements, Shadow DOM, and HTML templates — that let you build reusable, encapsulated UI elements with your own HTML tags that work in any framework or plain HTML page.
Learn Web Components and Custom Elements in our free JavaScript course — an interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
💡 Running Code Locally: Web Components are browser APIs ( customElements , attachShadow ), so they shine when run in a real page. For the best experience:
🧩 Real-World Analogy: A Web Component is like a LEGO brick :
Web Components let you create your own HTML elements that bundle structure, style, and behavior into a single reusable tag. Because they are built on browser standards rather than a specific library, a component you write once can be dropped into a React app, a Vue app, a server-rendered page, or a static HTML file without modification. The platform rests on three pillars: Custom Elements , Shadow DOM , and the <template> element.
🔥 Defining a Custom Element
A custom element is a JavaScript class that extends HTMLElement , registered with customElements.define() . The tag name must contain a hyphen so it can never collide with a standard HTML element.
Once defined, the browser upgrades every matching tag — current and future — into an instance of your class.
🔥 Lifecycle Callbacks
Custom elements expose lifecycle callbacks that the browser invokes automatically. The most important are connectedCallback (added to the DOM) and disconnectedCallback (removed). Use the constructor only for setup that does not touch attributes or children.
Cleaning up timers and listeners in disconnectedCallback prevents memory leaks when components are added and removed dynamically.
🔥 Observing Attributes
To react to attribute changes, declare a static observedAttributes getter listing the attribute names you care about. Only those attributes trigger attributeChangedCallback(name, oldValue, newValue) .
This keeps your component's rendering in sync with its attributes, the same way native elements respond to attributes like disabled or value .
🔥 Shadow DOM: Encapsulation
attachShadow() attaches a shadow root to your element — a separate DOM subtree whose markup and styles are isolated from the rest of the page. Outside CSS cannot reach in, and your component's styles cannot leak out.
With mode: "open" the shadow root is reachable via element.shadowRoot ; with mode: "closed" it is hidden from outside scripts. Encapsulation is what makes components safe to reuse anywhere without CSS conflicts.
🔥 Templates and Slots
The <template> element holds markup that is parsed but inert — it is not rendered until you clone it and insert it. Inside a shadow tree, a <slot> acts as a placeholder where the element's light-DOM children are projected, enabling composition.
Named slots ( slot="title" ) target a specific <slot name="title"> , while unnamed children fall into the default slot. The ::slotted() selector lets the component style projected content.
🔥 Autonomous vs Customized Built-in Elements
There are two kinds of custom elements. Autonomous elements extend HTMLElement and are brand-new tags. Customized built-in elements extend a specific native element (like HTMLButtonElement ) and are used with the is attribute, inheriting all the native behavior and accessibility.
Autonomous elements give you a clean slate; customized built-ins let you enhance native elements while keeping their semantics. Choose autonomous for new widgets and customized built-ins when you want to extend existing form controls or links.
🔥 Framework-Agnostic Reuse
Because custom elements are part of the platform, the same component works everywhere. Design systems like those built with Lit or Stencil ship Web Components precisely so a single library can serve React, Angular, Vue, and plain HTML teams at once.
This portability is the core promise of Web Components: write a UI element against the browser standard, and it outlives any single framework's lifecycle.
🔥 Common Web Components Mistakes to Avoid
❌ Anti-Patterns
🎯 Key Takeaways
Practice quiz
Which method registers a new custom element with the browser?
- customElements.define()
- document.createCustomElement()
- HTMLElement.register()
- window.defineElement()
Answer: customElements.define(). customElements.define('my-tag', MyClass) registers a custom element, mapping a tag name to a class that extends HTMLElement.
What naming rule must a custom element tag follow?
- It must be a single word
- It must be all uppercase
- It must contain a hyphen
- It must start with x-
Answer: It must contain a hyphen. Custom element names must contain a hyphen (e.g. user-card) to keep them distinct from current and future standard HTML elements.
Which lifecycle callback runs when the element is inserted into the DOM?
- renderedCallback()
- connectedCallback()
- mountedCallback()
- constructor()
Answer: connectedCallback(). connectedCallback() fires each time the element is added to the document, making it the right place to set up DOM and listeners.
How does a custom element subscribe to attribute changes?
- By calling watchAttributes()
- By adding an onattributechange handler
- Attributes are always observed automatically
- By defining a static observedAttributes array
Answer: By defining a static observedAttributes array. Only attributes listed in the static observedAttributes getter trigger attributeChangedCallback when they change.
What does element.attachShadow({ mode: 'open' }) create?
- A shadow root with encapsulated DOM
- A global style sheet
- A web worker
- A new iframe
Answer: A shadow root with encapsulated DOM. attachShadow creates a shadow root: an encapsulated DOM subtree whose markup and styles are scoped to the component.
What is the main benefit of Shadow DOM encapsulation?
- It disables JavaScript inside the component
- Its styles and markup are isolated from the rest of the page
- It forces server-side rendering
- It makes pages load faster automatically
Answer: Its styles and markup are isolated from the rest of the page. Shadow DOM scopes styles and structure so outside CSS does not leak in and the component's internals do not leak out.
What is the purpose of the <slot> element inside a shadow tree?
- To load external scripts
- To create a new shadow root
- To cache network responses
- To define a placeholder where light-DOM child content is projected
Answer: To define a placeholder where light-DOM child content is projected. A <slot> is a placeholder in the shadow DOM where the element's light-DOM children are projected, enabling composition.
Why is the <template> element well suited for components?
- It runs its scripts immediately on parse
- It is rendered twice for redundancy
- Its content is parsed but inert until cloned and inserted
- It automatically connects to a database
Answer: Its content is parsed but inert until cloned and inserted. A <template>'s content is parsed but not rendered or executed until you clone it (e.g. cloneNode(true)) and insert it.
What distinguishes a customized built-in element from an autonomous one?
- It must be written in TypeScript
- It extends a native element and is used with the is attribute
- It requires no class at all
- It works only inside iframes
Answer: It extends a native element and is used with the is attribute. A customized built-in extends a native element (e.g. class extends HTMLButtonElement) and is used via is='...', inheriting native behavior.
Why are Web Components considered framework-agnostic?
- They are built on browser standards, so they work in any framework or plain HTML
- They replace HTML entirely
- They only run in React
- They require a build step to function
Answer: They are built on browser standards, so they work in any framework or plain HTML. Web Components are native browser APIs, so a defined custom element can be reused in React, Vue, Angular, or plain HTML.