Web Components Deep Dive: Custom Elements, Shadow DOM, and Lifecycle
If you left web development before 2018, you might remember the browser wars over component standards— Polymer, X-Tag, and various polyfills fighting for dominance. Good news: the dust has settled. Web Components are a mature, widely-supported set of APIs that let you create reusable, encapsulated HTML elements.
This very blog uses 10 custom elements: <site-header>, <theme-toggle>,
<blog-post-card>, <code-block>, and more. No framework required—just
the platform. Let's dive deep into how they work.
What Are Web Components?
Web Components is an umbrella term for three main browser APIs:
- Custom Elements — Define your own HTML tags with custom behavior
- Shadow DOM — Encapsulate styles and markup so they don't leak out
- HTML Templates — Declare reusable markup that isn't rendered until needed
Together, these APIs let you build self-contained components that work anywhere HTML works— React apps, Vue projects, plain HTML pages, or even WordPress themes.
Your First Custom Element
Creating a custom element requires just two things: a class that extends
HTMLElement
and a call to customElements.define():
Hello, ${name}!
`; } } customElements.define('greeting-message', GreetingMessage);Now you can use it in HTML:
Naming rule: Custom element names must contain a hyphen (-). This distinguishes
them from built-in HTML elements and prevents future naming collisions. <my-button> is valid;
<mybutton> is not.
The Custom Element Lifecycle
Custom elements have four lifecycle callbacks that fire at specific moments. Understanding these is crucial for building components that work correctly:
constructor()
Called when the element is created—either by the HTML parser or by
document.createElement('my-element'). Use it for:
- Attaching Shadow DOM
- Setting up initial state
- Binding event handler methods to
this
Important: Don't access attributes, child elements, or DOM here. The element hasn't been added to the document yet, and attributes might not be parsed.
connectedCallback()
Called when the element is added to the document. This is your primary setup method—render content, add event listeners, fetch data, start animations:
Note: connectedCallback() can fire multiple times if an element is moved in the DOM
(removed and re-added). Design your setup logic to handle this.
disconnectedCallback()
Called when the element is removed from the document. Clean up here—remove event listeners, cancel timers, close connections:
attributeChangedCallback(name, oldValue, newValue)
Called when an observed attribute changes. You must declare which attributes to observe
using the static observedAttributes property:
Important: attributeChangedCallback fires before
connectedCallback for attributes present in the initial HTML. Guard against
accessing uninitialized DOM:
Shadow DOM: Style Encapsulation
Shadow DOM is what makes Web Components truly self-contained. It creates a separate DOM tree that:
- Isolates styles—CSS from outside doesn't affect the shadow tree, and vice versa
- Hides implementation details—
querySelectorwon't find shadow content - Provides scoped element IDs—no more worrying about ID collisions
Attaching Shadow DOM
This is encapsulated!
`; } }
The mode: 'open' option means JavaScript outside the component can access
the shadow DOM via element.shadowRoot. Use mode: 'closed' for
stricter encapsulation (though it's rarely needed).
The :host Selector
Inside shadow DOM, the :host
selector targets the custom element itself:
CSS Custom Properties Cross the Shadow Boundary
While regular CSS is encapsulated, CSS custom properties (variables) inherit into shadow DOM. This is the recommended way to make components themeable:
This blog's <theme-toggle> component uses exactly this pattern—it
reads --color-text, --color-surface, and --color-accent
from the document's CSS variables.
Slots: Composing Content
The <slot>
element lets users pass content into your component. It's the Web Components equivalent
of React's children prop:
My Custom Title
This goes into the default slot.
So does this!
Posted on Jan 3, 2026
Named slots (slot="header") go to their matching <slot name="header">.
Unnamed content fills the default slot. If no content is provided, the slot's fallback content shows.
Real-World Patterns
Let's look at patterns from production components on this very site.
Pattern 1: Reactive Attributes (blog-post-card)
This blog's <blog-post-card> component re-renders when any attribute changes:
${title}
Pattern 2: External State Communication (theme-toggle)
The <theme-toggle> component communicates with a global ThemeManager
and listens for custom events:
Pattern 3: Progressive Enhancement (reading-progress)
The <reading-progress> component uses scroll-driven animations when
available, with a JavaScript fallback:
Best Practices
1. Always Call super() First
2. Keep Constructor Light
Don't render, access attributes, or add event listeners in the constructor. Wait for
connectedCallback():
${this.getAttribute('name')}
`; // Might be null! } // Good constructor() { super(); this.attachShadow({ mode: 'open' }); } connectedCallback() { const name = this.getAttribute('name') || 'Default'; this.shadowRoot.innerHTML = `${name}
`; }3. Clean Up in disconnectedCallback
Any global event listeners, intervals, or connections should be cleaned up:
4. Use CSS Custom Properties for Theming
Don't hardcode colors. Use variables with sensible fallbacks:
5. Dispatch Custom Events for Communication
Use CustomEvent
to communicate with the outside world:
When to Use Web Components
Web Components shine when you need:
- Framework-agnostic components — Ship a design system that works in React, Vue, Angular, or plain HTML
- Style isolation — Widgets embedded in third-party sites (embeds, plugins)
- Zero dependencies — No build step, no framework, just the platform
- Future-proofing — Standards don't have major version breaking changes
Consider other options when:
- Your entire app is already in React/Vue—use their component model instead
- You need server-side rendering (SSR) — Web Components are client-side only
- SEO is critical and content must be in the initial HTML
Browser DevTools Tips
Debugging Web Components is straightforward with modern DevTools:
-
Elements panel: Shadow DOM appears as a
#shadow-rootnode you can expand and inspect -
Styles panel: Shows shadow DOM styles separately, with the
:hostselector clearly labeled -
Console: Use
$0.shadowRootto access the shadow DOM of a selected element - Break on: Right-click a custom element and use "Break on > subtree modifications" to debug rendering issues
Further Reading
- Web Components — MDN Web Docs
- Using Custom Elements — MDN Web Docs
- Using Shadow DOM — MDN Web Docs
- Using Templates and Slots — MDN Web Docs
- :host selector — MDN Web Docs
- CustomEvent — MDN Web Docs