Skip to content

Web Components Best Practices

This lesson explains Web Components Best Practices with clear examples, use cases, and best practices so you can build reusable Web Components confidently.

Web Components Best Practices Overview

Web Components best practices help developers build reusable, framework-independent, accessible, secure, and maintainable UI components using browser-native APIs.

Modern Web Components best practices include using clear custom element names, stable public APIs, Shadow DOM only when needed, lifecycle cleanup, CSS Custom Properties, CSS Parts, custom events, accessibility support, and strong documentation.

Web Components Best Practices Example

class AppButton extends HTMLElement {
  constructor() {
    super();

    this.attachShadow({ mode: "open" });

    this.handleClick = this.handleClick.bind(this);
  }

  connectedCallback() {
    this.render();

    this.shadowRoot
      .querySelector("button")
      .addEventListener("click", this.handleClick);
  }

  disconnectedCallback() {
    this.shadowRoot
      .querySelector("button")
      .removeEventListener("click", this.handleClick);
  }

  handleClick() {
    this.dispatchEvent(new CustomEvent("app-click", {
      detail: { clicked: true },
      bubbles: true,
      composed: true
    }));
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        button {
          background: var(--app-button-bg, #0d6efd);
          color: var(--app-button-color, white);
          border: none;
          border-radius: var(--app-button-radius, 0.5rem);
          padding: 0.5rem 1rem;
        }
      </style>

      <button type="button" part="button">
        <slot>Button</slot>
      </button>
    `;
  }
}

if (!customElements.get("app-button")) {
  customElements.define("app-button", AppButton);
}

This example uses a clear element name, Shadow DOM, CSS Custom Properties, CSS Parts, slots, custom events, lifecycle callbacks, and duplicate definition protection.

50 Web Components Best Practices with Examples

The following table lists important Web Components best practices for beginners, frontend developers, design system developers, and frontend architects.

# Best Practice Why It Matters Example Syntax
1Use hyphenated element namesRequired for valid custom elements.customElements.define("user-card", UserCard);
2Use clear component namesMakes components easier to understand.<app-button></app-button>
3Use PascalCase for element classesImproves class readability.class UserCard extends HTMLElement {}
4Extend HTMLElement for autonomous elementsCreates reusable custom HTML tags.class AppCard extends HTMLElement {}
5Call super first in constructorInitializes the parent element class.constructor() { super(); }
6Avoid heavy DOM work in constructorAttributes and children may not be ready.connectedCallback() { this.render(); }
7Use connectedCallback for setupRuns when the element is added to the page.connectedCallback() { this.render(); }
8Use disconnectedCallback for cleanupPrevents memory leaks.disconnectedCallback() { this.cleanup(); }
9Use observedAttributes carefullyTracks only attributes that need updates.static get observedAttributes() { return ["open"]; }
10Check values before re-renderingPrevents unnecessary work.if (oldValue === newValue) return;
11Prevent duplicate definitionsAvoids runtime errors.if (!customElements.get("app-card")) { customElements.define("app-card", AppCard); }
12Use customElements.whenDefinedWaits until a component is registered.customElements.whenDefined("app-card").then(init);
13Use Shadow DOM when encapsulation is neededKeeps internal markup and styles isolated.this.attachShadow({ mode: "open" });
14Prefer open Shadow DOM for reusable componentsEasier to inspect, test, and debug.this.attachShadow({ mode: "open" });
15Avoid closed Shadow DOM unless necessaryClosed mode makes debugging harder.// Prefer open mode for libraries
16Use slots for external contentAllows flexible content projection.<slot>Default content</slot>
17Use named slots for structured contentImproves component layout control.<slot name="title"></slot>
18Provide slot fallback contentImproves empty-state behavior.<slot>Default text</slot>
19Use CSS Custom Properties for themingAllows styling from outside Shadow DOM.color: var(--card-color, #111827);
20Provide CSS variable fallbacksKeeps components safe without custom themes.background: var(--button-bg, #0d6efd);
21Use CSS Parts for element-level stylingExposes safe styling hooks.<button part="button">Save</button>
22Treat part names as public APIChanging part names can break consumers.app-button::part(button)
23Use ::slotted only for direct slot contentAvoids styling confusion.::slotted(h2) { color: #0d6efd; }
24Dispatch custom events for communicationKeeps internal implementation private.this.dispatchEvent(new CustomEvent("item-selected"));
25Use kebab-case event namesMatches HTML and component naming style."item-selected"
26Use detail for event dataPasses useful payloads to consumers.detail: { id: 101 }
27Use bubbles for parent listenersAllows event delegation outside the component.bubbles: true
28Use composed for Shadow DOM eventsAllows events to cross Shadow DOM boundaries.composed: true
29Clean event listenersPrevents memory leaks.removeEventListener("click", this.handleClick);
30Clean observers and timersPrevents stale background work.observer.disconnect(); clearInterval(timerId);
31Use semantic HTML inside componentsImproves accessibility.<button type="button">Save</button>
32Support keyboard interactionMakes components usable without a mouse.element.addEventListener("keydown", handleKey);
33Manage focus carefullyImproves modal, menu, and form usability.this.shadowRoot.querySelector("button").focus();
34Reflect important state with attributesMakes state visible to CSS and HTML.this.toggleAttribute("open", isOpen);
35Sync attributes and properties safelyKeeps HTML and JavaScript APIs consistent.set value(val) { this.setAttribute("value", val); }
36Avoid unsafe innerHTML with user inputReduces XSS risks.message.textContent = userInput;
37Use templates for repeated markupImproves readability and reuse.const template = document.createElement("template");
38Cache DOM references when usefulReduces repeated queries.this.button = this.shadowRoot.querySelector("button");
39Batch DOM updatesImproves rendering performance.requestAnimationFrame(() => this.render());
40Avoid unnecessary re-rendersImproves performance and user experience.if (this.value === nextValue) return;
41Use Declarative Shadow DOM for SSRSupports HTML-first rendering.<template shadowrootmode="open"></template>
42Hydrate only when interactivity is neededReduces JavaScript cost.customElements.define("app-card", AppCard);
43Design stable public APIsProtects consumers from breaking changes.// Document attributes, properties, events, slots, and parts
44Document component usageImproves adoption across teams.// Example: <app-alert type="success">
45Version components carefullyMakes upgrades predictable.// Use semantic versioning
46Test accessibilityPrevents usability issues.// Test keyboard, screen reader, and contrast behavior
47Write component testsPrevents regressions.expect(element.shadowRoot).toBeTruthy();
48Test in real browsersWeb Components are browser APIs.// Use Playwright or Web Test Runner
49Keep dependencies minimalImproves portability and bundle size.// Prefer platform APIs when possible
50Build for framework interoperabilityAllows use in React, Angular, Vue, and vanilla apps.<app-button>Save</app-button>

Common Mistakes to Avoid

  • Defining a custom element name without a hyphen.
  • Calling customElements.define() multiple times for the same name.
  • Doing too much DOM work inside the constructor.
  • Forgetting to clean up event listeners, observers, timers, or intervals.
  • Using Shadow DOM when simple light DOM would be enough.
  • Using closed Shadow DOM when debugging and testing are important.
  • Expecting normal external CSS selectors to style private Shadow DOM elements.
  • Forgetting composed: true for public events from Shadow DOM.

Key Takeaways

  • Web Components best practices improve reuse, stability, and maintainability.
  • Use clear custom element names and stable public APIs.
  • Use Shadow DOM, slots, CSS Custom Properties, and CSS Parts intentionally.
  • Use custom events for clean communication with parent components.
  • Clean up listeners, observers, timers, and intervals.
  • Prioritize accessibility, documentation, testing, and framework interoperability.

Pro Tip

A good Web Component is not just reusable. It has a clear public API, accessible behavior, safe styling hooks, clean lifecycle management, and documentation that other teams can trust.