DOM API Polyfills
1. querySelector and querySelectorAll Implementation
| Method | Syntax | Returns | Browser Support |
|---|---|---|---|
| querySelector | element.querySelector(selector) |
First matching element or null | IE8+ |
| querySelectorAll | element.querySelectorAll(selector) |
Static NodeList of matching elements | IE8+ |
| document.querySelector | document.querySelector(selector) |
First matching element in document | Most common usage |
| Fallback: getElementById | document.getElementById(id) |
Element with specific ID | IE5+, universal support |
| Fallback: getElementsByClassName | element.getElementsByClassName(name) |
Live HTMLCollection | IE9+ (polyfillable for IE8) |
Example: querySelector polyfill for very old browsers
// querySelector polyfill (basic - uses Sizzle or native methods)
if (!document.querySelector) {
document.querySelector = function(selector) {
// Simple ID selector
if (selector.charAt(0) === '#') {
return document.getElementById(selector.slice(1));
}
// Simple class selector
if (selector.charAt(0) === '.') {
var elements = document.getElementsByClassName ?
document.getElementsByClassName(selector.slice(1)) :
document.getElementsByTagName('*');
return elements[0] || null;
}
// Simple tag selector
var elements = document.getElementsByTagName(selector);
return elements[0] || null;
};
}
// querySelectorAll polyfill (basic)
if (!document.querySelectorAll) {
document.querySelectorAll = function(selector) {
var style = document.createElement('style');
var elements = [];
var element;
document.documentElement.firstChild.appendChild(style);
document._qsa = [];
style.styleSheet.cssText = selector + '{x-qsa:expression(document._qsa && document._qsa.push(this))}';
window.scrollBy(0, 0);
while (document._qsa.length) {
element = document._qsa.shift();
element.style.removeAttribute('x-qsa');
elements.push(element);
}
document.documentElement.firstChild.removeChild(style);
return elements;
};
}
// Modern approach: Use Sizzle library for complex selectors
// https://github.com/jquery/sizzle
Example: getElementsByClassName polyfill for IE8
// getElementsByClassName polyfill
if (!document.getElementsByClassName) {
document.getElementsByClassName = function(className) {
return document.querySelectorAll('.' + className);
};
Element.prototype.getElementsByClassName = function(className) {
return this.querySelectorAll('.' + className);
};
}
// Alternative implementation without querySelectorAll
if (!document.getElementsByClassName) {
document.getElementsByClassName = function(className) {
var elements = document.getElementsByTagName('*');
var result = [];
var pattern = new RegExp('(^|\\s)' + className + '(\\s|$)');
for (var i = 0; i < elements.length; i++) {
if (pattern.test(elements[i].className)) {
result.push(elements[i]);
}
}
return result;
};
}
Note: For complex CSS selector support in legacy browsers, use Sizzle library (jQuery's selector engine) or limit to simple selectors (ID, class,
tag).
2. Element.classList API (add, remove, toggle)
| Method | Syntax | Description | Returns |
|---|---|---|---|
| classList.add | element.classList.add('class1', 'class2') |
Adds one or more classes | undefined |
| classList.remove | element.classList.remove('class1') |
Removes one or more classes | undefined |
| classList.toggle | element.classList.toggle('class') |
Toggles class presence | Boolean - true if added |
| classList.contains | element.classList.contains('class') |
Checks if class exists | Boolean |
| classList.replace | element.classList.replace('old', 'new') |
Replaces one class with another | Boolean - true if replaced |
| classList.item | element.classList.item(index) |
Returns class at index | String or null |
Example: classList polyfill for IE9
// classList polyfill
if (!('classList' in document.createElement('_'))) {
(function(view) {
'use strict';
if (!('Element' in view)) return;
var classListProp = 'classList',
protoProp = 'prototype',
elemCtrProto = view.Element[protoProp],
objCtr = Object,
strTrim = String[protoProp].trim || function() {
return this.replace(/^\s+|\s+$/g, '');
},
arrIndexOf = Array[protoProp].indexOf || function(item) {
var i = 0, len = this.length;
for (; i < len; i++) {
if (i in this && this[i] === item) {
return i;
}
}
return -1;
};
var DOMTokenList = function(elem) {
var classes = strTrim.call(elem.getAttribute('class') || '');
var tokens = classes ? classes.split(/\s+/) : [];
var i = 0;
var len = tokens.length;
for (; i < len; i++) {
this.push(tokens[i]);
}
this._updateClassName = function() {
elem.setAttribute('class', this.toString());
};
};
var classListProto = DOMTokenList[protoProp] = [];
classListProto.item = function(i) {
return this[i] || null;
};
classListProto.contains = function(token) {
token += '';
return arrIndexOf.call(this, token) !== -1;
};
classListProto.add = function() {
var tokens = arguments;
var i = 0;
var len = tokens.length;
var token;
var updated = false;
do {
token = tokens[i] + '';
if (arrIndexOf.call(this, token) === -1) {
this.push(token);
updated = true;
}
} while (++i < len);
if (updated) {
this._updateClassName();
}
};
classListProto.remove = function() {
var tokens = arguments;
var i = 0;
var len = tokens.length;
var token;
var updated = false;
var index;
do {
token = tokens[i] + '';
index = arrIndexOf.call(this, token);
while (index !== -1) {
this.splice(index, 1);
updated = true;
index = arrIndexOf.call(this, token);
}
} while (++i < len);
if (updated) {
this._updateClassName();
}
};
classListProto.toggle = function(token, force) {
token += '';
var result = this.contains(token);
var method = result ? force !== true && 'remove' : force !== false && 'add';
if (method) {
this[method](token);
}
return (force === true || force === false) ? force : !result;
};
classListProto.toString = function() {
return this.join(' ');
};
// Expose as property
if (objCtr.defineProperty) {
var classListPropDesc = {
get: function() {
return new DOMTokenList(this);
},
enumerable: true,
configurable: true
};
try {
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
} catch(ex) {
// IE8 doesn't support enumerable: true
objCtr.defineProperty(elemCtrProto, classListProp, {
get: classListPropDesc.get
});
}
}
}(window));
}
Note:
classList is supported in IE10+. For IE9 and below, this polyfill provides
full API compatibility including multiple class names in add/remove.
3. addEventListener and removeEventListener Polyfills
| Method | Standard | IE Legacy | Key Difference |
|---|---|---|---|
| Add Listener | addEventListener(type, fn, capture) |
attachEvent('on'+type, fn) |
Event name prefix, this context |
| Remove Listener | removeEventListener(type, fn, capture) |
detachEvent('on'+type, fn) |
Must use same function reference |
| Event Object | Passed as parameter | window.event |
Access method differs |
| Prevent Default | event.preventDefault() |
event.returnValue = false |
Different property |
| Stop Propagation | event.stopPropagation() |
event.cancelBubble = true |
Different property |
| Event Target | event.target |
event.srcElement |
Different property name |
Example: addEventListener polyfill for IE8
// addEventListener polyfill for IE8
if (!Element.prototype.addEventListener) {
Element.prototype.addEventListener = function(type, fn, capture) {
var element = this;
// Create wrapper to fix context and event object
var wrapper = function(e) {
e = e || window.event;
// Fix event object
e.target = e.target || e.srcElement;
e.currentTarget = element;
e.preventDefault = e.preventDefault || function() {
e.returnValue = false;
};
e.stopPropagation = e.stopPropagation || function() {
e.cancelBubble = true;
};
// Call with correct context
return fn.call(element, e);
};
// Store wrapper for removal
if (!element._eventListeners) {
element._eventListeners = {};
}
if (!element._eventListeners[type]) {
element._eventListeners[type] = [];
}
element._eventListeners[type].push({
listener: fn,
wrapper: wrapper
});
// Attach event
element.attachEvent('on' + type, wrapper);
};
Element.prototype.removeEventListener = function(type, fn, capture) {
if (!this._eventListeners || !this._eventListeners[type]) {
return;
}
var listeners = this._eventListeners[type];
for (var i = 0; i < listeners.length; i++) {
if (listeners[i].listener === fn) {
this.detachEvent('on' + type, listeners[i].wrapper);
listeners.splice(i, 1);
break;
}
}
};
// Also add to Window and Document
if (!window.addEventListener) {
Window.prototype.addEventListener = Element.prototype.addEventListener;
Window.prototype.removeEventListener = Element.prototype.removeEventListener;
Document.prototype.addEventListener = Element.prototype.addEventListener;
Document.prototype.removeEventListener = Element.prototype.removeEventListener;
}
}
// Event object normalization
function normalizeEvent(e) {
e = e || window.event;
if (!e.target) e.target = e.srcElement;
if (!e.currentTarget) e.currentTarget = this;
if (!e.relatedTarget) e.relatedTarget = e.fromElement || e.toElement;
if (!e.which) e.which = e.charCode || e.keyCode;
if (!e.preventDefault) {
e.preventDefault = function() {
e.returnValue = false;
};
}
if (!e.stopPropagation) {
e.stopPropagation = function() {
e.cancelBubble = true;
};
}
return e;
}
Warning: IE8's
attachEvent executes handlers in reverse
order and with different this context (window instead of element). The polyfill fixes
these issues.
4. CustomEvent and Event Constructor API
| Constructor | Syntax | Properties | Use Case |
|---|---|---|---|
| Event | new Event(type, options) |
bubbles, cancelable, composed | Basic event creation |
| CustomEvent | new CustomEvent(type, options) |
Event properties + detail | Events with custom data |
| MouseEvent | new MouseEvent(type, options) |
Mouse-specific properties | Simulate mouse interactions |
| KeyboardEvent | new KeyboardEvent(type, options) |
Key, keyCode, modifiers | Simulate keyboard input |
| Legacy: createEvent | document.createEvent('Event') |
initEvent required | IE9-11 fallback |
Example: CustomEvent polyfill
// CustomEvent polyfill
(function() {
if (typeof window.CustomEvent === 'function') return false;
function CustomEvent(event, params) {
params = params || { bubbles: false, cancelable: false, detail: null };
var evt = document.createEvent('CustomEvent');
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
return evt;
}
window.CustomEvent = CustomEvent;
})();
// Event constructor polyfill
(function() {
if (typeof window.Event === 'function') return false;
function Event(event, params) {
params = params || { bubbles: false, cancelable: false };
var evt = document.createEvent('Event');
evt.initEvent(event, params.bubbles, params.cancelable);
return evt;
}
window.Event = Event;
})();
// Usage examples
var customEvent = new CustomEvent('myevent', {
detail: { message: 'Hello World' },
bubbles: true,
cancelable: true
});
element.dispatchEvent(customEvent);
// Listen for custom event
element.addEventListener('myevent', function(e) {
console.log(e.detail.message); // 'Hello World'
});
Example: MouseEvent and KeyboardEvent polyfills
// MouseEvent polyfill
(function() {
try {
new MouseEvent('test');
return false;
} catch(e) {}
var MouseEventPolyfill = function(eventType, params) {
params = params || { bubbles: false, cancelable: false };
var mouseEvent = document.createEvent('MouseEvent');
mouseEvent.initMouseEvent(
eventType,
params.bubbles,
params.cancelable,
window,
params.detail || 0,
params.screenX || 0,
params.screenY || 0,
params.clientX || 0,
params.clientY || 0,
params.ctrlKey || false,
params.altKey || false,
params.shiftKey || false,
params.metaKey || false,
params.button || 0,
params.relatedTarget || null
);
return mouseEvent;
};
MouseEventPolyfill.prototype = Event.prototype;
window.MouseEvent = MouseEventPolyfill;
})();
// KeyboardEvent polyfill
(function() {
try {
new KeyboardEvent('test');
return false;
} catch(e) {}
var KeyboardEventPolyfill = function(eventType, params) {
params = params || { bubbles: false, cancelable: false };
var keyboardEvent = document.createEvent('KeyboardEvent');
var initMethod = keyboardEvent.initKeyboardEvent || keyboardEvent.initKeyEvent;
if (initMethod) {
initMethod.call(
keyboardEvent,
eventType,
params.bubbles,
params.cancelable,
window,
params.key || '',
params.location || 0,
params.ctrlKey || false,
params.altKey || false,
params.shiftKey || false,
params.metaKey || false
);
}
return keyboardEvent;
};
KeyboardEventPolyfill.prototype = Event.prototype;
window.KeyboardEvent = KeyboardEventPolyfill;
})();
5. Element.matches and Selector API
| Method | Standard Name | Vendor Prefixes | Purpose |
|---|---|---|---|
| matches | element.matches(selector) |
matchesSelector, msMatchesSelector | Test if element matches selector |
| closest | element.closest(selector) |
No prefix needed | Find nearest ancestor matching selector |
| Vendor Prefixes | - | webkit, moz, ms, o | Browser-specific implementations |
Example: Element.matches polyfill with vendor prefixes
// Element.matches polyfill
if (!Element.prototype.matches) {
Element.prototype.matches =
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector ||
function(s) {
var matches = (this.document || this.ownerDocument).querySelectorAll(s);
var i = matches.length;
while (--i >= 0 && matches.item(i) !== this) {}
return i > -1;
};
}
// Usage
if (element.matches('.active')) {
console.log('Element is active');
}
if (element.matches('div.container > p')) {
console.log('Element matches selector');
}
Example: Element.closest polyfill
// Element.closest polyfill
if (!Element.prototype.closest) {
Element.prototype.closest = function(s) {
var el = this;
do {
if (Element.prototype.matches.call(el, s)) {
return el;
}
el = el.parentElement || el.parentNode;
} while (el !== null && el.nodeType === 1);
return null;
};
}
// Usage
var button = document.querySelector('button');
var form = button.closest('form');
var container = button.closest('.container');
// Event delegation with closest
document.addEventListener('click', function(e) {
var button = e.target.closest('button');
if (button) {
console.log('Button clicked:', button);
}
});
Note:
closest is extremely useful for event
delegation. It traverses up the DOM tree until it finds a matching element or reaches the document
root.
6. Node.contains and DOM Traversal Methods
| Method | Syntax | Returns | Use Case |
|---|---|---|---|
| contains | node.contains(otherNode) |
Boolean - true if descendant | Check if node is inside another |
| compareDocumentPosition | node.compareDocumentPosition(other) |
Bitmask of position relationship | Detailed position comparison |
| childElementCount | element.childElementCount |
Number of child elements | Count element children (not text nodes) |
| firstElementChild | element.firstElementChild |
First child element | Skip text nodes |
| lastElementChild | element.lastElementChild |
Last child element | Skip text nodes |
| nextElementSibling | element.nextElementSibling |
Next sibling element | Navigate between elements |
| previousElementSibling | element.previousElementSibling |
Previous sibling element | Navigate between elements |
Example: Node.contains polyfill
// Node.contains polyfill
if (!Node.prototype.contains) {
Node.prototype.contains = function(node) {
if (!(0 in arguments)) {
throw new TypeError('1 argument is required');
}
do {
if (this === node) {
return true;
}
} while (node = node && node.parentNode);
return false;
};
}
// Usage
var container = document.getElementById('container');
var element = document.getElementById('child');
if (container.contains(element)) {
console.log('Element is inside container');
}
// Useful for click outside detection
document.addEventListener('click', function(e) {
var dropdown = document.getElementById('dropdown');
if (!dropdown.contains(e.target)) {
// Click was outside dropdown
closeDropdown();
}
});
Example: Element traversal property polyfills
// firstElementChild polyfill
if (!('firstElementChild' in document.documentElement)) {
Object.defineProperty(Element.prototype, 'firstElementChild', {
get: function() {
var el = this.firstChild;
while (el && el.nodeType !== 1) {
el = el.nextSibling;
}
return el;
}
});
}
// lastElementChild polyfill
if (!('lastElementChild' in document.documentElement)) {
Object.defineProperty(Element.prototype, 'lastElementChild', {
get: function() {
var el = this.lastChild;
while (el && el.nodeType !== 1) {
el = el.previousSibling;
}
return el;
}
});
}
// nextElementSibling polyfill
if (!('nextElementSibling' in document.documentElement)) {
Object.defineProperty(Element.prototype, 'nextElementSibling', {
get: function() {
var el = this.nextSibling;
while (el && el.nodeType !== 1) {
el = el.nextSibling;
}
return el;
}
});
}
// previousElementSibling polyfill
if (!('previousElementSibling' in document.documentElement)) {
Object.defineProperty(Element.prototype, 'previousElementSibling', {
get: function() {
var el = this.previousSibling;
while (el && el.nodeType !== 1) {
el = el.previousSibling;
}
return el;
}
});
}
// childElementCount polyfill
if (!('childElementCount' in document.documentElement)) {
Object.defineProperty(Element.prototype, 'childElementCount', {
get: function() {
var count = 0;
var child = this.firstChild;
while (child) {
if (child.nodeType === 1) count++;
child = child.nextSibling;
}
return count;
}
});
}
Key Takeaways - DOM APIs
- querySelector/All: Use Sizzle for complex selector support in old browsers
- classList: Full polyfill available for IE9, provides clean class manipulation
- addEventListener: Polyfill normalizes IE8 attachEvent differences
- CustomEvent: Use createEvent fallback for IE9-11
- matches/closest: Essential for event delegation and selector testing
- Element traversal: Polyfills skip text nodes for cleaner DOM navigation