Fast and lightweight polyfill for min/max-width CSS3 Media Queries for IE 6-8 and more
—
The matchMedia addListener extension provides event listener functionality for media query state changes. This enables JavaScript code to respond dynamically to viewport changes, such as device orientation or window resizing.
Extends the basic MediaQueryList with listener support for monitoring state changes.
interface MediaQueryListWithListeners extends MediaQueryList {
/**
* Add listener for media query state changes
* Callback is invoked when the query transitions between matching and non-matching
* @param listener - Function called when match state changes
*/
addListener(listener: (mql: MediaQueryList) => void): void;
/**
* Remove previously added listener
* @param listener - Exact listener function to remove
*/
removeListener(listener: (mql: MediaQueryList) => void): void;
}Register callbacks that fire when media query match state transitions occur.
/**
* Add listener for media query state changes
* The listener is called only when the query transitions between matching and non-matching states
* @param listener - Callback function receiving the MediaQueryList object
*/
addListener(listener: (mql: MediaQueryList) => void): void;Usage Examples:
// Create a media query with listener support
var desktopQuery = matchMedia('(min-width: 1024px)');
// Add listener for desktop/mobile transitions
desktopQuery.addListener(function(mql) {
if (mql.matches) {
console.log('Switched to desktop view');
// Enable desktop-specific features
enableDesktopMenu();
loadDesktopWidgets();
} else {
console.log('Switched to mobile view');
// Enable mobile-specific features
enableMobileMenu();
unloadDesktopWidgets();
}
});
// Orientation change detection
var landscapeQuery = matchMedia('(orientation: landscape)');
landscapeQuery.addListener(function(mql) {
if (mql.matches) {
document.body.classList.add('landscape');
document.body.classList.remove('portrait');
} else {
document.body.classList.add('portrait');
document.body.classList.remove('landscape');
}
});Remove previously registered listeners to prevent memory leaks.
/**
* Remove previously added listener
* Must pass the exact same function reference that was used with addListener
* @param listener - Previously registered listener function
*/
removeListener(listener: (mql: MediaQueryList) => void): void;Usage Examples:
// Store reference to listener function
function handleDesktopChange(mql) {
if (mql.matches) {
enableDesktopFeatures();
} else {
disableDesktopFeatures();
}
}
var desktopQuery = matchMedia('(min-width: 1024px)');
// Add listener
desktopQuery.addListener(handleDesktopChange);
// Later, remove the listener
desktopQuery.removeListener(handleDesktopChange);
// Example: Cleanup when component unmounts
function cleanup() {
desktopQuery.removeListener(handleDesktopChange);
tabletQuery.removeListener(handleTabletChange);
mobileQuery.removeListener(handleMobileChange);
}The listener system includes built-in debouncing to prevent excessive callback execution during window resizing.
// Internal listener tracking (conceptual)
var queries = []; // Array of {mql, listeners} objects
var isListening = false; // Global resize listener state
var timeoutID = 0; // Debounce timeout referenceListeners are only called when the media query transitions between matching and non-matching states:
// Example transition scenarios:
// Window resized from 800px to 1200px width
// Query: '(min-width: 1024px)'
// Result: Listener called with matches: true
// Window resized from 1200px to 900px width
// Query: '(min-width: 1024px)'
// Result: Listener called with matches: false
// Window resized from 800px to 850px width
// Query: '(min-width: 1024px)'
// Result: No listener call (state unchanged)function initResponsiveComponent() {
var breakpoints = {
mobile: matchMedia('(max-width: 767px)'),
tablet: matchMedia('(min-width: 768px) and (max-width: 1023px)'),
desktop: matchMedia('(min-width: 1024px)')
};
// Set up listeners for each breakpoint
breakpoints.mobile.addListener(function(mql) {
if (mql.matches) {
switchToMobileLayout();
}
});
breakpoints.tablet.addListener(function(mql) {
if (mql.matches) {
switchToTabletLayout();
}
});
breakpoints.desktop.addListener(function(mql) {
if (mql.matches) {
switchToDesktopLayout();
}
});
// Initialize with current state
if (breakpoints.desktop.matches) {
switchToDesktopLayout();
} else if (breakpoints.tablet.matches) {
switchToTabletLayout();
} else {
switchToMobileLayout();
}
}var highDPIQuery = matchMedia('(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)');
highDPIQuery.addListener(function(mql) {
var images = document.querySelectorAll('img[data-src-2x]');
images.forEach(function(img) {
if (mql.matches) {
// Load high-resolution images
img.src = img.getAttribute('data-src-2x');
} else {
// Load standard resolution images
img.src = img.getAttribute('data-src');
}
});
});var printQuery = matchMedia('print');
printQuery.addListener(function(mql) {
if (mql.matches) {
// Prepare for print
hidePrintIncompatibleElements();
expandCollapsedSections();
generatePrintFooter();
} else {
// Return from print preview
showPrintIncompatibleElements();
restoreCollapsedSections();
removePrintFooter();
}
});// Check for native addListener support
if (window.matchMedia && matchMedia('all').addListener) {
console.log('Native addListener support detected');
} else {
console.log('Using addListener polyfill');
}
// The polyfill handles this automatically
var mql = matchMedia('(min-width: 768px)');
mql.addListener(callback); // Works in all browsers// Good: Store listener references for cleanup
var listeners = [];
function addResponsiveListener(query, handler) {
var mql = matchMedia(query);
mql.addListener(handler);
listeners.push({mql: mql, handler: handler});
}
function removeAllListeners() {
listeners.forEach(function(item) {
item.mql.removeListener(item.handler);
});
listeners = [];
}The built-in 30ms debouncing is usually sufficient, but for expensive operations, add additional throttling:
var desktopQuery = matchMedia('(min-width: 1024px)');
var isProcessing = false;
desktopQuery.addListener(function(mql) {
if (isProcessing) return;
isProcessing = true;
setTimeout(function() {
// Expensive layout operations
if (mql.matches) {
initializeDesktopComponents();
} else {
teardownDesktopComponents();
}
isProcessing = false;
}, 100);
});Install with Tessl CLI
npx tessl i tessl/npm-respond-js