Note: Custom element names must contain hyphen (e.g., "my-element" not "myelement"). Constructor runs before element attached to DOM - don't access children or attributes there. Use connectedCallback for setup. Customized built-ins not supported in Safari.
Warning: Don't call super() without arguments in constructor. Don't add attributes in constructor - use connectedCallback. connectedCallback can be called multiple times if element moved. Always cleanup in disconnectedCallback.
2. Shadow DOM Encapsulation and Styling
Method/Property
Description
Browser Support
element.attachShadow(options)
Creates shadow root. Options: mode ("open" or "closed"), delegatesFocus.
All Modern Browsers
element.shadowRoot
Returns shadow root if mode is "open", null if "closed".
All Modern Browsers
shadowRoot.mode
"open" or "closed". Determines if shadowRoot accessible from outside.
All Modern Browsers
shadowRoot.host
Returns host element of shadow root.
All Modern Browsers
Shadow DOM Feature
Description
Benefit
Style Encapsulation
Styles inside shadow DOM don't affect outside, and vice versa.
Prevent CSS conflicts
DOM Encapsulation
Shadow DOM not accessible via querySelector from outside.
Implementation hiding
Event Retargeting
Events from shadow DOM appear to come from host element.
Note: Use mode: "open" for better accessibility and debugging. Shadow DOM styles don't inherit from outside except inheritable properties (color, font-family, etc.). Use ::part() to expose styling hooks. :host-context() has limited browser support.
Warning: Not all elements can have shadow DOM (e.g., <input>, <img>). Closed shadow DOM still accessible via DevTools - not security feature. Events from shadow DOM appear to originate from host (event retargeting).
3. HTML Templates and Template Element
Element/Property
Description
Browser Support
<template>
HTML element that holds client-side content not rendered on page load.
All Browsers
template.content
Returns DocumentFragment containing template's DOM subtree.
All Browsers
document.importNode(node, deep)
Clones node from another document. Use with template content.
Note: Template content is inert - scripts don't run, images don't load, styles don't apply until cloned into document. Use cloneNode(true) to deep clone template content. Templates can be defined in HTML or created programmatically.
4. Slot API for Content Projection
Element/Attribute
Description
Use Case
<slot>
Placeholder for projected content from light DOM.
Content projection
<slot name="...">
Named slot for specific content.
Multiple projection points
slot="name"
Attribute on light DOM element to target named slot.
Target specific slot
Slot Method/Property
Description
slot.assignedNodes(options)
Returns array of nodes assigned to slot. Options: flatten (include nested slots).
slot.assignedElements(options)
Returns array of elements (not text nodes) assigned to slot.
element.assignedSlot
Returns <slot> element that element is assigned to.
Note: Slots enable content projection - light DOM content rendered in shadow DOM. Unnamed slots are default slots. Multiple elements can target same named slot. Use ::slotted() to style slotted content from shadow DOM.
Warning:::slotted() can only select direct children, not descendants. Slotted elements remain in light DOM - only rendered location changes. Slot fallback content shown when no content assigned to slot.
5. Element Internals for Form Integration
Method/Property
Description
Browser Support
element.attachInternals()
Returns ElementInternals object for form/accessibility integration.
All Modern Browsers
static formAssociated = true
Marks element as form-associated. Required for form integration.
All Modern Browsers
ElementInternals Property/Method
Description
form
Returns associated <form> element.
setFormValue(value, state)
Sets element's form value. Optional state for internal representation.
Browser restores state. Mode: "restore" or "autocomplete".
Example: Form-associated custom element
// Custom input element with form integrationclass CustomInput extends HTMLElement { // Must set formAssociated to true static formAssociated = true; constructor() { super(); // Attach internals for form integration this._internals = this.attachInternals(); const shadow = this.attachShadow({ "mode": "open" }); shadow.innerHTML = ` <style> input { padding: 8px; border: 1px solid #ccc; border-radius: 4px; } input:invalid { border-color: red; } .error { color: red; font-size: 12px; } </style> <input type="text" /> <div class="error"></div> `; this._input = shadow.querySelector("input"); this._error = shadow.querySelector(".error"); // Sync internal input with form value this._input.addEventListener("input", () => { this.value = this._input.value; }); } // Form lifecycle callbacks formAssociatedCallback(form) { console.log("Associated with form:", form); } formResetCallback() { console.log("Form reset"); this.value = ""; } formDisabledCallback(disabled) { console.log("Disabled:", disabled); this._input.disabled = disabled; } formStateRestoreCallback(state, mode) { console.log("State restore:", state, mode); this.value = state; } // Value getter/setter get value() { return this._input.value; } set value(val) { this._input.value = val; // Update form value this._internals.setFormValue(val); // Validate this.validate(); } // Validation validate() { const value = this.value; if (this.hasAttribute("required") && !value) { this._internals.setValidity( { "valueMissing": true }, "This field is required", this._input ); this._error.textContent = "This field is required"; return false; } const minLength = parseInt(this.getAttribute("minlength")) || 0; if (value.length < minLength) { this._internals.setValidity( { "tooShort": true }, `Minimum ${minLength} characters required`, this._input ); this._error.textContent = `Minimum ${minLength} characters required`; return false; } // Valid this._internals.setValidity({}); this._error.textContent = ""; return true; } // Standard form methods checkValidity() { return this._internals.checkValidity(); } reportValidity() { return this._internals.reportValidity(); } // Access to form get form() { return this._internals.form; } get validity() { return this._internals.validity; } get validationMessage() { return this._internals.validationMessage; }}customElements.define("custom-input", CustomInput);// Usage:// <form>// <custom-input name="username" required minlength="3"></custom-input>// <button type="submit">Submit</button>// </form>// Access form valueconst form = document.querySelector("form");form.addEventListener("submit", (e) => { e.preventDefault(); const formData = new FormData(form); console.log("Username:", formData.get("username"));});
Note: Set static formAssociated = true to enable form integration. Use attachInternals() to get ElementInternals object. Custom elements integrate with native form features - validation, FormData, submit events. Use setFormValue() to sync with form.
Warning: Can only call attachInternals() once. Must set formAssociated before element registered. Form callbacks may not fire in all scenarios - test thoroughly. ElementInternals has good browser support but check for older browsers.
Note: Adoptable Stylesheets enable stylesheet reuse across shadow roots - better performance than duplicating <style> tags. Stylesheets created once, shared across components. Use replaceSync() for initial load, replace() for async updates.
Warning: Changes to shared stylesheet affect all components using it - be careful with dynamic updates. adoptedStyleSheets replaces array - use spread to add: [...shadow.adoptedStyleSheets, newSheet]. Constructable stylesheets not available in older browsers.
Web Components Best Practices
Always use hyphenated names for custom elements - required by spec
Use mode: "open" for shadow DOM - better accessibility and debugging
Initialize in constructor, setup in connectedCallback
Always cleanup in disconnectedCallback - remove listeners, cancel timers
Use attributeChangedCallback with observedAttributes for reactive attributes
Use templates to avoid creating DOM strings repeatedly
Use slots for content projection - more flexible than imperative DOM manipulation
Use ::part() to expose styling hooks for component consumers
Set formAssociated = true for form-integrated custom elements
Use adoptable stylesheets to share styles across components efficiently
Avoid customized built-ins - not supported in Safari
Test components in isolation and with various content projections