// Custom Elements v1 polyfill (simplified)(function() { if ('customElements' in window) { return; // Native support } var CustomElementRegistry = function() { this._definitions = new Map(); this._whenDefinedPromises = new Map(); this._upgradeQueue = []; }; CustomElementRegistry.prototype.define = function(name, constructor, options) { // Validate tag name if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)+$/.test(name)) { throw new Error('Invalid custom element name: ' + name); } if (this._definitions.has(name)) { throw new Error('Element already defined: ' + name); } // Store definition this._definitions.set(name, { name: name, constructor: constructor, options: options || {} }); // Resolve whenDefined promise if (this._whenDefinedPromises.has(name)) { this._whenDefinedPromises.get(name).resolve(); this._whenDefinedPromises.delete(name); } // Upgrade existing elements this._upgradeElements(name); }; CustomElementRegistry.prototype.get = function(name) { var def = this._definitions.get(name); return def ? def.constructor : undefined; }; CustomElementRegistry.prototype.whenDefined = function(name) { if (this._definitions.has(name)) { return Promise.resolve(); } if (!this._whenDefinedPromises.has(name)) { var resolve, reject; var promise = new Promise(function(res, rej) { resolve = res; reject = rej; }); promise.resolve = resolve; promise.reject = reject; this._whenDefinedPromises.set(name, promise); } return this._whenDefinedPromises.get(name); }; CustomElementRegistry.prototype._upgradeElements = function(name) { var elements = document.getElementsByTagName(name); for (var i = 0; i < elements.length; i++) { this._upgradeElement(elements[i], name); } }; CustomElementRegistry.prototype._upgradeElement = function(element, name) { if (element.__upgraded) { return; } var definition = this._definitions.get(name); if (!definition) { return; } // Create instance var instance = new definition.constructor(); // Copy attributes for (var i = 0; i < element.attributes.length; i++) { var attr = element.attributes[i]; instance.setAttribute(attr.name, attr.value); } // Copy children while (element.firstChild) { instance.appendChild(element.firstChild); } // Replace element element.parentNode.replaceChild(instance, element); // Mark as upgraded instance.__upgraded = true; // Call connectedCallback if present if (instance.connectedCallback) { instance.connectedCallback(); } }; CustomElementRegistry.prototype._observeNewElements = function() { var self = this; if (!window.MutationObserver) { return; } var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { mutation.addedNodes.forEach(function(node) { if (node.nodeType === Node.ELEMENT_NODE) { var name = node.tagName.toLowerCase(); if (self._definitions.has(name)) { self._upgradeElement(node, name); } } }); }); }); observer.observe(document.documentElement, { childList: true, subtree: true }); }; // Create global registry var registry = new CustomElementRegistry(); registry._observeNewElements(); Object.defineProperty(window, 'customElements', { value: registry, writable: false, configurable: true }); // Polyfill HTMLElement if needed if (typeof HTMLElement !== 'function') { window.HTMLElement = function() {}; }})();// Usage exampleclass MyButton extends HTMLElement { constructor() { super(); this.addEventListener('click', this._handleClick.bind(this)); } connectedCallback() { this.innerHTML = '<button>' + (this.getAttribute('label') || 'Click me') + '</button>'; } disconnectedCallback() { // Cleanup } static get observedAttributes() { return ['label']; } attributeChangedCallback(name, oldValue, newValue) { if (name === 'label') { this.querySelector('button').textContent = newValue; } } _handleClick() { this.dispatchEvent(new CustomEvent('my-click', { detail: { message: 'Clicked!' } })); }}customElements.define('my-button', MyButton);
Note: Use @webcomponents/webcomponentsjs polyfill package for production. It
includes Custom Elements, Shadow DOM, and HTML Templates polyfills.
2. Shadow DOM v1 Polyfill and Limitations
Feature
API
Support
Polyfill Limitations
attachShadow()
element.attachShadow({ mode: 'open' })
Modern browsers
Not true encapsulation
Shadow Root
element.shadowRoot
Access shadow content
Open mode only in polyfill
Style Encapsulation
Scoped styles inside shadow
Native isolation
Scoped class names instead
Slot Elements
<slot> for content projection
Native support
Polyfill redistributes content
:host selector
Target host element from shadow
Native support
Replaced with scoped classes
::slotted()
Style slotted content
Native support
Limited polyfill support
Example: Shadow DOM polyfill (simplified)
// Shadow DOM v1 polyfill (simplified concept)(function() { if ('attachShadow' in Element.prototype) { return; // Native support } var ShadowRoot = function(host, mode) { this.host = host; this.mode = mode; this._innerHTML = ''; this._children = []; // Create shadow container this._container = document.createElement('div'); this._container.className = '__shadow-root__'; this._container.style.display = 'contents'; // Pass-through layout }; // Implement appendChild, removeChild, etc. ShadowRoot.prototype.appendChild = function(node) { this._children.push(node); this._container.appendChild(node); return node; }; Object.defineProperty(ShadowRoot.prototype, 'innerHTML', { get: function() { return this._container.innerHTML; }, set: function(html) { this._container.innerHTML = html; this._scopeStyles(); this._processSlots(); } }); ShadowRoot.prototype._scopeStyles = function() { var styles = this._container.querySelectorAll('style'); var scopeId = this.host.getAttribute('data-shadow-id') || 'shadow-' + Math.random().toString(36).substr(2, 9); this.host.setAttribute('data-shadow-id', scopeId); for (var i = 0; i < styles.length; i++) { var style = styles[i]; var css = style.textContent; // Scope selectors css = css.replace(/:host\b/g, '[data-shadow-id="' + scopeId + '"]'); css = css.replace(/([^{}]+)\{/g, function(match, selectors) { if (selectors.indexOf('@') === 0) return match; // Skip at-rules var scoped = selectors.split(',').map(function(sel) { sel = sel.trim(); if (sel.indexOf('[data-shadow-id') >= 0) return sel; return '[data-shadow-id="' + scopeId + '"] ' + sel; }).join(', '); return scoped + ' {'; }); style.textContent = css; } // Add scope to all elements var elements = this._container.querySelectorAll('*'); for (var i = 0; i < elements.length; i++) { elements[i].setAttribute('data-shadow-child', scopeId); } }; ShadowRoot.prototype._processSlots = function() { var slots = this._container.querySelectorAll('slot'); var host = this.host; for (var i = 0; i < slots.length; i++) { var slot = slots[i]; var slotName = slot.getAttribute('name') || ''; // Find content for this slot var content; if (slotName) { content = host.querySelectorAll('[slot="' + slotName + '"]'); } else { // Default slot gets all children without slot attribute content = Array.prototype.filter.call(host.childNodes, function(node) { return !node.getAttribute || !node.getAttribute('slot'); }); } // Move content into slot while (slot.firstChild) { slot.removeChild(slot.firstChild); } for (var j = 0; j < content.length; j++) { slot.appendChild(content[j].cloneNode(true)); } } }; // Polyfill attachShadow Element.prototype.attachShadow = function(options) { if (this.__shadowRoot) { throw new Error('Shadow root already attached'); } var mode = options && options.mode || 'open'; var shadowRoot = new ShadowRoot(this, mode); this.__shadowRoot = shadowRoot; // Clear host content and append shadow container while (this.firstChild) { shadowRoot._originalContent = shadowRoot._originalContent || []; shadowRoot._originalContent.push(this.firstChild); this.removeChild(this.firstChild); } this.appendChild(shadowRoot._container); return shadowRoot; }; // Polyfill shadowRoot getter Object.defineProperty(Element.prototype, 'shadowRoot', { get: function() { return this.__shadowRoot && this.__shadowRoot.mode === 'open' ? this.__shadowRoot : null; } });})();// Usage exampleclass ShadowCard extends HTMLElement { constructor() { super(); var shadow = this.attachShadow({ mode: 'open' }); shadow.innerHTML = ` <style> :host { display: block; border: 1px solid #ccc; border-radius: 4px; padding: 16px; } .header { font-weight: bold; margin-bottom: 8px; } .content { color: #666; } </style> <div class="header"> <slot name="header">Default Header</slot> </div> <div class="content"> <slot>Default Content</slot> </div> `; }}customElements.define('shadow-card', ShadowCard);
Warning: Shadow DOM polyfills cannot provide true style
encapsulation. Global styles may still leak in. Consider using CSS Modules or scoped CSS instead for
better polyfill support.
3. HTML Templates and Template Element Polyfills
Feature
API
Browser Support
Polyfill Method
<template>
Inert content container
IE not supported
Hidden div fallback
content property
template.content
DocumentFragment
Create fragment manually
cloneNode()
template.content.cloneNode(true)
Clone template content
Standard cloning works
Inert Scripts
Scripts don't execute in template
Native behavior
Remove type or src attributes
Example: HTML Template polyfill
// HTML Template element polyfill(function() { if ('content' in document.createElement('template')) { return; // Native support } // Polyfill template element var templates = document.getElementsByTagName('template'); for (var i = 0; i < templates.length; i++) { var template = templates[i]; // Create DocumentFragment for content var content = document.createDocumentFragment(); var child; while (child = template.firstChild) { content.appendChild(child); } // Store content as property Object.defineProperty(template, 'content', { get: function() { return this._content || (this._content = content); } }); // Hide template template.style.display = 'none'; } // Observe new templates if (window.MutationObserver) { var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { mutation.addedNodes.forEach(function(node) { if (node.tagName === 'TEMPLATE') { initTemplate(node); } }); }); }); observer.observe(document.documentElement, { childList: true, subtree: true }); } function initTemplate(template) { if (template.content) return; var content = document.createDocumentFragment(); var child; while (child = template.firstChild) { content.appendChild(child); } Object.defineProperty(template, 'content', { get: function() { return this._content || (this._content = content); } }); template.style.display = 'none'; }})();// Usage examplevar template = document.getElementById('my-template');var clone = document.importNode(template.content, true);document.body.appendChild(clone);// HTML:// <template id="my-template">// <div class="card">// <h3>Title</h3>// <p>Content</p>// </div>// </template>
Note: The <template> element is well-supported in modern browsers. For IE,
use the polyfill or fallback to hidden <div> containers.
4. Slot Element and Content Projection
Feature
Syntax
Description
Polyfill
Default Slot
<slot></slot>
Unnamed slot for default content
Content redistribution
Named Slot
<slot name="header"></slot>
Target specific content
Match by slot attribute
Slotted Content
<div slot="header">
Assign content to slot
Move/clone content
Fallback Content
Content inside <slot>
Shown if no slotted content
Keep if no match found
assignedNodes()
slot.assignedNodes()
Get slotted nodes
Track assigned content
slotchange event
Fires when slot content changes
React to content updates
MutationObserver
Example: Slot element polyfill
// Slot element polyfill (simplified)var SlotPolyfill = { processSlots: function(shadowRoot, hostElement) { var slots = shadowRoot.querySelectorAll('slot'); for (var i = 0; i < slots.length; i++) { this.processSlot(slots[i], hostElement); } }, processSlot: function(slot, hostElement) { var slotName = slot.getAttribute('name'); var assignedNodes = []; if (slotName) { // Named slot - find matching content var matches = hostElement.querySelectorAll('[slot="' + slotName + '"]'); assignedNodes = Array.prototype.slice.call(matches); } else { // Default slot - find unassigned content assignedNodes = Array.prototype.filter.call(hostElement.childNodes, function(node) { if (node.nodeType === Node.TEXT_NODE) { return node.textContent.trim() !== ''; } return !node.getAttribute || !node.getAttribute('slot'); }); } // Store assigned nodes for API slot._assignedNodes = assignedNodes; // Clear slot while (slot.firstChild) { slot.removeChild(slot.firstChild); } // Insert assigned content or fallback if (assignedNodes.length > 0) { assignedNodes.forEach(function(node) { slot.appendChild(node.cloneNode(true)); }); } // If no assigned nodes, slot keeps its fallback content }, // Implement assignedNodes() method polyfillAssignedNodes: function() { if (!('assignedNodes' in HTMLSlotElement.prototype)) { HTMLSlotElement.prototype.assignedNodes = function(options) { return this._assignedNodes || []; }; } if (!('assignedElements' in HTMLSlotElement.prototype)) { HTMLSlotElement.prototype.assignedElements = function(options) { var nodes = this._assignedNodes || []; return nodes.filter(function(node) { return node.nodeType === Node.ELEMENT_NODE; }); }; } }, watchSlotChanges: function(slot, hostElement) { if (!window.MutationObserver) return; var self = this; var observer = new MutationObserver(function(mutations) { self.processSlot(slot, hostElement); // Dispatch slotchange event var event = document.createEvent('Event'); event.initEvent('slotchange', true, false); slot.dispatchEvent(event); }); observer.observe(hostElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['slot'] }); }};// Usage in custom elementclass SlottedCard extends HTMLElement { constructor() { super(); var shadow = this.attachShadow({ mode: 'open' }); shadow.innerHTML = ` <style> .card { border: 1px solid #ccc; padding: 16px; } .header { font-size: 18px; font-weight: bold; margin-bottom: 8px; } .footer { margin-top: 8px; font-size: 12px; color: #666; } </style> <div class="card"> <div class="header"> <slot name="header">Default Header</slot> </div> <div class="content"> <slot>Default content goes here</slot> </div> <div class="footer"> <slot name="footer">Default Footer</slot> </div> </div> `; // Process slots with polyfill if needed if (!('assignedNodes' in HTMLSlotElement.prototype)) { SlotPolyfill.processSlots(shadow, this); } }}customElements.define('slotted-card', SlottedCard);// HTML usage:// <slotted-card>// <h2 slot="header">My Title</h2>// <p>This is my content</p>// <span slot="footer">Copyright 2025</span>// </slotted-card>
Note: Slot polyfills work by cloning content, not moving it.
Event listeners on original content won't work. Re-attach listeners after slotting.
5. HTML Imports Alternative Implementations
Technology
Status
Alternative
Use Case
HTML Imports
DEPRECATED
ES Modules
Component loading
<link rel="import">
Never standardized
fetch() + innerHTML
Load HTML fragments
ES Modules
Modern browsers
<script type="module">
Preferred method
Dynamic import()
Modern browsers
import('./component.js')
Lazy loading
Template Includes
No native support
Build tools / SSR
Server-side composition
Example: HTML Imports alternative with fetch
// HTML Imports alternative using fetchvar HTMLLoader = { cache: new Map(), import: function(url) { // Return cached promise if exists if (this.cache.has(url)) { return this.cache.get(url); } // Fetch and parse HTML var promise = fetch(url) .then(function(response) { if (!response.ok) { throw new Error('Failed to load: ' + url); } return response.text(); }) .then(function(html) { // Parse HTML into document fragment var template = document.createElement('template'); template.innerHTML = html; return { url: url, content: template.content, querySelector: function(selector) { return template.content.querySelector(selector); }, querySelectorAll: function(selector) { return template.content.querySelectorAll(selector); } }; }); this.cache.set(url, promise); return promise; }, importAll: function(urls) { return Promise.all(urls.map(this.import.bind(this))); }};// UsageHTMLLoader.import('/components/card.html') .then(function(imported) { // Get template from imported document var template = imported.querySelector('template#card-template'); var clone = document.importNode(template.content, true); document.body.appendChild(clone); }) .catch(function(error) { console.error('Import failed:', error); });// Load multiple componentsHTMLLoader.importAll([ '/components/header.html', '/components/footer.html', '/components/sidebar.html']).then(function(imports) { console.log('All components loaded');});
Example: ES Module approach for Web Components
// Modern approach: ES Modules for Web Components// component.jsexport class MyComponent extends HTMLElement { constructor() { super(); var shadow = this.attachShadow({ mode: 'open' }); shadow.innerHTML = this.getTemplate(); } getTemplate() { return ` <style> :host { display: block; padding: 16px; } </style> <div class="component"> <slot></slot> </div> `; } connectedCallback() { console.log('Component connected'); }}// Auto-register when module loadscustomElements.define('my-component', MyComponent);// main.js - Import and useimport { MyComponent } from './component.js';// Component is auto-registered, just use itdocument.body.innerHTML = '<my-component>Hello!</my-component>';// Or lazy loadasync function loadComponent() { const module = await import('./component.js'); console.log('Component loaded and registered');}// Load on demanddocument.getElementById('load-btn').addEventListener('click', loadComponent);
Warning: HTML Imports are deprecated and removed from browsers.
Use ES Modules with import statements or dynamic import() for component loading.
6. Scoped CSS and Style Encapsulation
Method
Approach
Browser Support
Pros/Cons
Shadow DOM
Native style encapsulation
Modern browsers
True isolation, polyfill limitations
Scoped Attribute
<style scoped>
REMOVED FROM SPEC
Never fully supported
CSS Modules
Build-time scoping
All browsers
Requires build step
BEM Naming
Naming convention
All browsers
Manual, no true isolation
CSS-in-JS
JavaScript-generated styles
All browsers
Runtime overhead
Data Attributes
Scope with unique attributes
All browsers
Simple, no build tools
Example: Scoped CSS polyfill with data attributes
// Scoped CSS polyfillvar ScopedCSS = { scopeId: 0, applyScoped: function(container) { var scopeId = 'scope-' + (++this.scopeId); // Add scope attribute to container container.setAttribute('data-scope', scopeId); // Find style elements var styles = container.querySelectorAll('style[scoped]'); for (var i = 0; i < styles.length; i++) { this.scopeStyle(styles[i], scopeId); } // Add scope to all descendants var elements = container.querySelectorAll('*'); for (var i = 0; i < elements.length; i++) { if (elements[i].tagName !== 'STYLE') { elements[i].setAttribute('data-scope-child', scopeId); } } }, scopeStyle: function(styleElement, scopeId) { var css = styleElement.textContent; // Scope all selectors css = css.replace(/([^{}]+)\{/g, function(match, selectors) { // Skip at-rules if (selectors.trim().indexOf('@') === 0) { return match; } var scoped = selectors.split(',').map(function(selector) { selector = selector.trim(); // Scope the selector return '[data-scope="' + scopeId + '"] ' + selector; }).join(', '); return scoped + ' {'; }); styleElement.textContent = css; styleElement.removeAttribute('scoped'); // Move style to head for better performance if (styleElement.parentNode !== document.head) { document.head.appendChild(styleElement); } }, // Auto-process on DOM ready init: function() { var containers = document.querySelectorAll('[data-scoped-css]'); for (var i = 0; i < containers.length; i++) { this.applyScoped(containers[i]); } // Watch for new containers this.observeDOM(); }, observeDOM: function() { if (!window.MutationObserver) return; var self = this; var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { mutation.addedNodes.forEach(function(node) { if (node.nodeType === Node.ELEMENT_NODE) { if (node.hasAttribute && node.hasAttribute('data-scoped-css')) { self.applyScoped(node); } } }); }); }); observer.observe(document.documentElement, { childList: true, subtree: true }); }};// Initializeif (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function() { ScopedCSS.init(); });} else { ScopedCSS.init();}// Usage:// <div data-scoped-css>// <style scoped>// .title { color: blue; }// p { font-size: 14px; }// </style>// // <h1 class="title">Scoped Title</h1>// <p>Scoped paragraph</p>// </div>
Example: CSS-in-JS approach for components
// Simple CSS-in-JS for Web Componentsclass StyledComponent extends HTMLElement { constructor() { super(); this.scopeId = 'component-' + Math.random().toString(36).substr(2, 9); this.setAttribute('data-component-id', this.scopeId); this.injectStyles(); this.render(); } getStyles() { return { '.container': { padding: '16px', border: '1px solid #ccc', borderRadius: '4px' }, '.title': { fontSize: '18px', fontWeight: 'bold', marginBottom: '8px' }, '.content': { color: '#666' } }; } injectStyles() { var styles = this.getStyles(); var css = ''; // Convert styles object to CSS string for (var selector in styles) { var scopedSelector = '[data-component-id="' + this.scopeId + '"] ' + selector; css += scopedSelector + ' {'; var rules = styles[selector]; for (var prop in rules) { var cssProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase(); css += cssProp + ': ' + rules[prop] + ';'; } css += '}'; } // Create and inject style element var styleEl = document.createElement('style'); styleEl.textContent = css; document.head.appendChild(styleEl); // Store reference for cleanup this._styleElement = styleEl; } render() { this.innerHTML = ` <div class="container"> <div class="title">${this.getAttribute('title') || 'Title'}</div> <div class="content"> <slot></slot> </div> </div> `; } disconnectedCallback() { // Cleanup styles when component removed if (this._styleElement && this._styleElement.parentNode) { this._styleElement.parentNode.removeChild(this._styleElement); } }}customElements.define('styled-component', StyledComponent);// Usage:// <styled-component title="My Component">// This is the content// </styled-component>
Key Takeaways - Web Components
Custom Elements: Use @webcomponents/custom-elements polyfill for IE11