What are Web Components?

Web Components are browser-native APIs for creating reusable, encapsulated HTML elements — no framework required. They work in all modern browsers and can be used inside React, Vue, Angular, or plain HTML.

Three core technologies:

  1. Custom Elements — define new HTML tags with lifecycle callbacks
  2. Shadow DOM — encapsulate styles and markup from the page
  3. HTML Templates — inert, reusable markup fragments

Custom Elements

  <script>
class GreetingCard extends HTMLElement {
    static observedAttributes = ['name'];

    connectedCallback() {
        this.render();
    }

    attributeChangedCallback(name, oldVal, newVal) {
        if (oldVal !== newVal) this.render();
    }

    render() {
        const name = this.getAttribute('name') || 'World';
        this.textContent = '';
        const p = document.createElement('p');
        p.textContent = `Hello, ${name}!`;
        this.appendChild(p);
    }
}
customElements.define('greeting-card', GreetingCard);
</script>

<greeting-card name="Alice"></greeting-card>
  

Lifecycle Callbacks

Callback When
connectedCallback Element added to DOM
disconnectedCallback Element removed
attributeChangedCallback Observed attribute changes
adoptedCallback Element moved to new document

Register with customElements.define('tag-name', Class). Tag names must contain a hyphen.

Shadow DOM

Styles inside Shadow DOM don’t leak out; page styles don’t leak in:

  class UserBadge extends HTMLElement {
    constructor() {
        super();
        const shadow = this.attachShadow({ mode: 'open' });
        shadow.innerHTML = `
            <style>
                span {
                    background: #007bff;
                    color: white;
                    padding: 4px 8px;
                    border-radius: 4px;
                    font-size: 0.875rem;
                }
            </style>
            <span><slot></slot></span>
        `;
    }
}
customElements.define('user-badge', UserBadge);
  
  <user-badge>Admin</user-badge>
  
  • mode: 'open'element.shadowRoot accessible (recommended for debugging)
  • mode: 'closed' — shadow root hidden from outside JS

HTML Templates

Templates are inert until cloned — ideal for repeated structures:

  <template id="card-template">
    <div class="card">
        <h3 class="title"></h3>
        <p class="body"></p>
    </div>
</template>

<script>
const template = document.getElementById('card-template');
const clone = template.content.cloneNode(true);
clone.querySelector('.title').textContent = 'Hello';
clone.querySelector('.body').textContent = 'World';
document.body.appendChild(clone);
</script>
  

Slots — Content Projection

  shadow.innerHTML = `
    <style>.wrapper { border: 1px solid #ccc; padding: 1rem; }</style>
    <div class="wrapper">
        <slot name="header"></slot>
        <slot></slot>
        <slot name="footer"></slot>
    </div>
`;
  
  <my-card>
    <h2 slot="header">Title</h2>
    <p>Body content</p>
    <small slot="footer">Updated today</small>
</my-card>
  

Unnamed <slot> is the default slot. Use ::slotted(p) in CSS to style slotted content (limited styling).

Attributes and Properties

  class ToggleButton extends HTMLElement {
    static get observedAttributes() { return ['pressed']; }

    get pressed() {
        return this.hasAttribute('pressed');
    }
    set pressed(val) {
        val ? this.setAttribute('pressed', '') : this.removeAttribute('pressed');
    }
}
  

Reflect important state to attributes for CSS styling: [pressed] { background: green; }.

When to Use Web Components

Good fit Less ideal
Design systems shared across frameworks Simple static pages
Embeddable widgets (chat, payment) App entirely in one framework
Browser extensions Need SSR without hydration setup
Long-lived component libraries Rapid prototyping

Frameworks like Lit, Shoelace, and FAST simplify reactive properties and efficient rendering.

Best Practices

  1. Use Shadow DOM for style encapsulation in design systems
  2. Observed attributes for declarative API (<tabs selected="2">)
  3. Progressive enhancement — component works without JS where possible
  4. Accessible by default — keyboard support, ARIA, focus management
  5. Lazy definecustomElements.define before first use, or use customElements.whenDefined()

Troubleshooting

Component not rendering

  • Ensure class extends HTMLElement and super() is called in constructor
  • Tag name must include a hyphen; define before using in HTML

Styles not applying inside component

  • Styles in Shadow DOM don’t affect light DOM; put component styles inside shadow <style>

Form elements not participating in forms

  • Use ElementInternals and formAssociated for form-aware custom elements

Web Components are the web platform’s answer to reusable UI — learn them for cross-framework design systems and embeddable widgets.