Skip to content

Common Web Components Mistakes

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

Web Components Common Mistakes Overview

Web Components are powerful, but small mistakes in naming, lifecycle callbacks, Shadow DOM, slots, styling, events, and accessibility can make components hard to reuse and maintain.

Understanding common mistakes helps you build stable, accessible, framework-independent components that work well across applications and teams.

Common Mistake Example

class BadButton extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `
      <button>${this.getAttribute("label")}</button>
    `;

    window.addEventListener("resize", function() {
      console.log("resize");
    });
  }
}

customElements.define("badbutton", BadButton);

This example has multiple problems: the element name has no hyphen, untrusted attribute content is inserted with innerHTML, and the resize listener is never cleaned up.

50 Web Components Common Mistakes with Fixes

# Common Mistake Why It Is a Problem Better Approach
1Using a name without a hyphenCustom element names must contain a hyphen.customElements.define("app-card", AppCard);
2Defining the same element twiceDuplicate definitions throw an error.if (!customElements.get("app-card")) { customElements.define("app-card", AppCard); }
3Forgetting to extend HTMLElementThe browser cannot create a proper custom element class.class AppCard extends HTMLElement {}
4Forgetting super()The parent element class is not initialized.constructor() { super(); }
5Doing heavy work in constructorAttributes and children may not be ready yet.connectedCallback() { this.render(); }
6Rendering repeatedly in connectedCallbackThe callback can run more than once if the element is moved.if (this.hasRendered) return;
7Not cleaning up listenersCan cause memory leaks and duplicate behavior.disconnectedCallback() { this.removeEventListener("click", this.handleClick); }
8Using anonymous listener functionsAnonymous functions are difficult to remove later.this.handleClick = this.handleClick.bind(this);
9Not disconnecting observersObservers can keep running after the component is removed.this.observer.disconnect();
10Not clearing timersTimers can keep stale component references alive.clearInterval(this.timerId);
11Watching too many attributesUnnecessary attribute changes cause extra work.static get observedAttributes() { return ["open"]; }
12Forgetting observedAttributesattributeChangedCallback() will not run.static get observedAttributes() { return ["value"]; }
13Creating attribute-property loopsCan trigger repeated updates or infinite loops.if (oldValue === newValue) return;
14Using attributes for complex objectsAttributes are strings and are not ideal for objects.element.data = userObject;
15Not reflecting important stateCSS and external users cannot see component state.this.toggleAttribute("open", isOpen);
16Using Shadow DOM for everythingSome simple components do not need encapsulation.Use light DOM when global styling is required.
17Using closed Shadow DOM by defaultMakes debugging, testing, and integration harder.this.attachShadow({ mode: "open" });
18Expecting global CSS to style Shadow DOMNormal external selectors cannot reach private Shadow DOM.Use CSS Custom Properties or ::part().
19Hardcoding all Shadow DOM stylesConsumers cannot theme the component.background: var(--card-bg, white);
20Forgetting CSS variable fallbacksComponent styles may break when values are missing.color: var(--text-color, #111827);
21Exposing internal class names as APIClass names are implementation details and may change.<button part="button">
22Forgetting the part attribute::part() cannot style anything without it.<h2 part="title">Title</h2>
23Changing part names oftenBreaks consumer styling.Treat part names as public API.
24Using unclear part namesMakes styling hooks confusing.part="header"
25Expecting ::slotted() to style deeply nested children::slotted() only targets direct slotted elements.::slotted(h2) { color: #0d6efd; }
26Not providing slot fallback contentComponent may look empty when no content is passed.<slot>Default content</slot>
27Using slots without documenting themConsumers will not know how to pass content.Document slot names like title, icon, footer.
28Dispatching events without useful namesEvents like change can be unclear or conflict."value-changed"
29Forgetting bubbles: trueParent components may not receive the event.bubbles: true
30Forgetting composed: trueEvents may not cross Shadow DOM boundaries.composed: true
31Putting too much data in event detailLarge or unrelated payloads make APIs hard to maintain.detail: { id, value }
32Not making cancelable events when neededConsumers cannot stop important actions.cancelable: true
33Ignoring event retargetingOutside listeners may see the host instead of internal elements.event.composedPath()
34Using unsafe innerHTMLCan create XSS security risks.element.textContent = userInput;
35Trusting frontend validation onlyUsers can bypass frontend validation.Validate again on the server.
36Using non-semantic markupHurts accessibility.<button type="button">Save</button>
37Ignoring keyboard supportComponent becomes difficult to use without a mouse.handle Enter, Space, Escape, and Tab.
38Not managing focusMenus, modals, and dialogs may be inaccessible.this.shadowRoot.querySelector("button").focus();
39Using ARIA incorrectlyBad ARIA can make accessibility worse.Prefer semantic HTML first.
40Not testing with screen readers or keyboardAutomated tests do not catch everything.Test keyboard navigation and focus states.
41Re-rendering too muchCan reduce performance and reset focus or state.Update only changed DOM nodes.
42Querying DOM repeatedlyUnnecessary queries add overhead.this.button = this.shadowRoot.querySelector("button");
43Not batching DOM updatesCan cause layout thrashing.requestAnimationFrame(() => this.render());
44Adding large dependenciesReduces portability and increases bundle size.Prefer browser-native APIs when possible.
45Not testing in real browsersWeb Components rely on browser platform behavior.Use Playwright or browser-based tests.
46Not documenting public APIsConsumers may misuse the component.Document attributes, properties, events, slots, parts, and CSS variables.
47Changing APIs without migration notesBreaks applications using the component.Use semantic versioning and migration guides.
48Ignoring framework interoperabilityComponents may be hard to use in React, Angular, or Vue.Test usage across target frameworks.
49Using Declarative Shadow DOM without fallback strategyOlder environments may need progressive enhancement.Use feature detection and hydration carefully.
50Building components without ownershipNo one maintains bugs, docs, or upgrades.Define owners, support process, and release rules.

Most Important Mistakes to Avoid

  • Using custom element names without a hyphen.
  • Defining the same custom element multiple times.
  • Forgetting lifecycle cleanup in disconnectedCallback().
  • Expecting external CSS to style private Shadow DOM.
  • Forgetting bubbles: true and composed: true for public events.
  • Using unsafe innerHTML with user-provided content.
  • Ignoring accessibility, keyboard support, and focus management.
  • Not documenting attributes, properties, events, slots, parts, and CSS variables.

Key Takeaways

  • Web Components need stable names, APIs, and lifecycle management.
  • Use Shadow DOM only when encapsulation is useful.
  • Use CSS Custom Properties and CSS Parts for safe styling hooks.
  • Use custom events for clean communication with parent code.
  • Clean up listeners, observers, timers, and intervals.
  • Accessibility, testing, documentation, and framework compatibility are essential.

Pro Tip

Treat every Web Component like a public API. Once other teams use your attributes, events, slots, parts, and CSS variables, changing them without planning can break real applications.