The Universal Remote Control
Imagine creating a universal remote control that needs to work with TVs from the 1990s all the way to the latest smart TVs. Each TV has different capabilities - older ones might only respond to basic commands, while newer ones support voice control and internet connectivity. This is exactly the challenge we face with browser compatibility!
Understanding Browser Differences
Before we can create compatible code, we need to understand what makes browsers different:
1. JavaScript Engine Differences
| Browser | JavaScript Engine | Key Characteristics |
|---|---|---|
| Chrome | V8 | Fast execution, good ES6+ support |
| Firefox | SpiderMonkey | Good standards compliance |
| Safari | JavaScriptCore | Sometimes slower to adopt features |
| Edge (Chromium) | V8 | Similar to Chrome |
| Internet Explorer 11 | Chakra | Limited ES6 support |
2. CSS Support Variations
/* Modern CSS with fallbacks */
.container {
/* Fallback for older browsers */
display: block;
/* Modern browsers */
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
/* Feature detection with @supports */
@supports (display: grid) {
.container {
display: grid;
}
}
/* Vendor prefixes for older browsers */
.animation {
-webkit-animation: slide 1s ease;
-moz-animation: slide 1s ease;
animation: slide 1s ease;
}
3. API Availability
// Check for API availability before using
if ('IntersectionObserver' in window) {
// Use IntersectionObserver
const observer = new IntersectionObserver(callback);
} else {
// Fall back to scroll events
window.addEventListener('scroll', fallbackFunction);
}
// Fetch API with fallback
if (window.fetch) {
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data));
} else {
// Fallback to XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data');
xhr.onload = () => console.log(JSON.parse(xhr.responseText));
xhr.send();
}
Feature Detection Strategies
Feature detection is the cornerstone of browser compatibility. Instead of checking which browser is being used, we check if specific features are available:
1. Native Feature Detection
// JavaScript feature detection
const supportsPromises = typeof Promise !== 'undefined';
const supportsArrowFunctions = (() => {
try {
eval('() => {}');
return true;
} catch (e) {
return false;
}
})();
// DOM API detection
const supportsFetch = 'fetch' in window;
const supportsWebGL = (() => {
try {
const canvas = document.createElement('canvas');
return !!(window.WebGLRenderingContext &&
(canvas.getContext('webgl') || canvas.getContext('experimental-webgl')));
} catch(e) {
return false;
}
})();
// CSS feature detection
const supportsGrid = CSS.supports('display', 'grid');
const supportsCustomProperties = CSS.supports('--custom', 'value');
2. Using Modernizr
// Install Modernizr
npm install modernizr
// Create a custom build with only needed tests
// modernizr-config.json
{
"minify": true,
"options": [
"setClasses",
"addTest",
"html5printshiv",
"testProp",
"fnBind"
],
"feature-detects": [
"css/flexbox",
"css/grid",
"es6/promises",
"serviceworker"
]
}
// Using Modernizr in code
if (Modernizr.flexbox) {
// Use flexbox
element.style.display = 'flex';
} else {
// Use fallback layout
element.style.display = 'table';
}
// CSS classes automatically added to HTML
/* In your CSS */
.flexbox .container {
display: flex;
}
.no-flexbox .container {
display: table;
}
3. Custom Feature Detection
// Create a feature detection utility
const features = {
// Check for Passive Event Listeners
passiveEvents: (() => {
let supportsPassive = false;
try {
const opts = Object.defineProperty({}, 'passive', {
get: function() {
supportsPassive = true;
return true;
}
});
window.addEventListener('testPassive', null, opts);
window.removeEventListener('testPassive', null, opts);
} catch (e) {}
return supportsPassive;
})(),
// Check for Service Worker
serviceWorker: 'serviceWorker' in navigator,
// Check for Touch Events
touch: 'ontouchstart' in window || navigator.maxTouchPoints > 0,
// Check for Local Storage
localStorage: (() => {
try {
localStorage.setItem('test', 'test');
localStorage.removeItem('test');
return true;
} catch(e) {
return false;
}
})()
};
// Use the feature detection
if (features.passiveEvents) {
element.addEventListener('scroll', handler, { passive: true });
} else {
element.addEventListener('scroll', handler);
}
Polyfill Strategies
Polyfills add missing functionality to older browsers. Let's explore different approaches:
1. Manual Polyfill Implementation
// Array.prototype.includes polyfill
if (!Array.prototype.includes) {
Array.prototype.includes = function(searchElement, fromIndex) {
if (this == null) {
throw new TypeError('"this" is null or not defined');
}
const o = Object(this);
const len = o.length >>> 0;
if (len === 0) {
return false;
}
const n = fromIndex | 0;
let k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
while (k < len) {
if (o[k] === searchElement) {
return true;
}
k++;
}
return false;
};
}
// Promise polyfill (simplified)
if (typeof Promise === 'undefined') {
window.Promise = function(executor) {
let onResolve, onReject;
let fulfilled = false;
let rejected = false;
let value;
this.then = function(onFulfilled, onRejected) {
if (fulfilled) {
onFulfilled(value);
} else {
onResolve = onFulfilled;
}
if (rejected) {
onRejected(value);
} else {
onReject = onRejected;
}
return this;
};
function resolve(val) {
value = val;
fulfilled = true;
if (onResolve) {
onResolve(value);
}
}
function reject(reason) {
value = reason;
rejected = true;
if (onReject) {
onReject(value);
}
}
executor(resolve, reject);
};
}
2. Using Polyfill.io
3. Dynamic Polyfill Loading
// Feature-based dynamic loading
async function loadPolyfills() {
const polyfills = [];
if (!window.Promise) {
polyfills.push(import('promise-polyfill'));
}
if (!window.fetch) {
polyfills.push(import('whatwg-fetch'));
}
if (!Element.prototype.closest) {
polyfills.push(import('element-closest-polyfill'));
}
if (polyfills.length > 0) {
await Promise.all(polyfills);
}
}
// Load polyfills before app initialization
loadPolyfills().then(() => {
// Initialize your application
initApp();
});
// Using dynamic import with feature detection
if (!window.IntersectionObserver) {
import('intersection-observer').then(() => {
// Now we can use IntersectionObserver
setupLazyLoading();
});
} else {
setupLazyLoading();
}
Progressive Enhancement vs Graceful Degradation
Two philosophical approaches to browser compatibility:
Progressive Enhancement Example
Graceful Degradation Example
// Start with full functionality
class ImageGallery {
constructor(element) {
this.element = element;
this.images = Array.from(element.querySelectorAll('img'));
// Check for required features
if (this.checkSupport()) {
this.initAdvancedGallery();
} else {
this.initBasicGallery();
}
}
checkSupport() {
return 'IntersectionObserver' in window &&
'fetch' in window &&
CSS.supports('display', 'grid');
}
initAdvancedGallery() {
// Full experience with lazy loading and animations
this.setupLazyLoading();
this.setupAnimations();
this.setupTouchGestures();
}
initBasicGallery() {
// Degraded experience for older browsers
this.setupBasicNavigation();
this.loadAllImages();
}
setupLazyLoading() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
}
});
});
this.images.forEach(img => observer.observe(img));
}
setupBasicNavigation() {
// Simple previous/next buttons
const prevButton = document.createElement('button');
const nextButton = document.createElement('button');
prevButton.textContent = 'Previous';
nextButton.textContent = 'Next';
// Add basic click handlers
prevButton.onclick = () => this.navigate(-1);
nextButton.onclick = () => this.navigate(1);
}
}
Cross-Browser Testing Strategies
Testing across browsers is crucial for ensuring compatibility:
1. Manual Testing Checklist
// Browser Testing Checklist
const testingChecklist = {
browsers: [
'Chrome (latest)',
'Firefox (latest)',
'Safari (latest)',
'Edge (latest)',
'Internet Explorer 11',
'Mobile Safari (iOS)',
'Chrome (Android)'
],
features: [
'Page layout',
'Form submission',
'JavaScript functionality',
'CSS animations',
'Responsive design',
'Touch interactions',
'Keyboard navigation',
'Screen reader compatibility'
],
scenarios: [
'Fast connection',
'Slow connection',
'JavaScript disabled',
'CSS disabled',
'Different screen sizes',
'Different input methods'
]
};
2. Automated Testing
// Using Selenium WebDriver for cross-browser testing
const { Builder, By, until } = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');
const firefox = require('selenium-webdriver/firefox');
async function runCrossBrowserTest() {
const browsers = [
{ name: 'chrome', builder: new Builder().forBrowser('chrome') },
{ name: 'firefox', builder: new Builder().forBrowser('firefox') }
];
for (const browser of browsers) {
const driver = await browser.builder.build();
try {
await driver.get('http://localhost:3000');
// Test page load
await driver.wait(until.titleIs('My App'), 5000);
// Test form submission
await driver.findElement(By.id('username')).sendKeys('testuser');
await driver.findElement(By.id('password')).sendKeys('password');
await driver.findElement(By.css('button[type="submit"]')).click();
// Verify navigation
await driver.wait(until.urlContains('/dashboard'), 5000);
console.log(`✓ Tests passed on ${browser.name}`);
} catch (error) {
console.error(`✗ Tests failed on ${browser.name}:`, error);
} finally {
await driver.quit();
}
}
}
// Using Playwright for modern cross-browser testing
const { chromium, firefox, webkit } = require('playwright');
async function testWithPlaywright() {
const browsers = [chromium, firefox, webkit];
for (const browserType of browsers) {
const browser = await browserType.launch();
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('http://localhost:3000');
// Test interactions
await page.fill('#username', 'testuser');
await page.fill('#password', 'password');
await page.click('button[type="submit"]');
// Verify results
await page.waitForSelector('.dashboard');
await browser.close();
}
}
3. Browser Testing Services
// BrowserStack configuration
// browserstack.json
{
"auth": {
"username": "YOUR_USERNAME",
"access_key": "YOUR_ACCESS_KEY"
},
"browsers": [
{
"browser": "chrome",
"browser_version": "latest",
"os": "Windows",
"os_version": "10"
},
{
"browser": "safari",
"browser_version": "14.0",
"os": "OS X",
"os_version": "Big Sur"
},
{
"browser": "firefox",
"browser_version": "latest",
"os": "Windows",
"os_version": "10"
}
],
"run_settings": {
"cypress_config_file": "./cypress.json",
"project_name": "My Web App",
"build_name": "Cross-browser Testing",
"parallels": 5
}
}
Handling CSS Compatibility
CSS compatibility requires special attention:
1. Feature Queries
/* Base styles for all browsers */
.container {
display: block;
margin: 0 auto;
max-width: 1200px;
}
/* Progressive enhancement with feature queries */
@supports (display: grid) {
.container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
}
/* Fallback for flexbox if grid not supported */
@supports (display: flex) and (not (display: grid)) {
.container {
display: flex;
flex-wrap: wrap;
}
.container > * {
flex: 1 1 300px;
margin: 10px;
}
}
/* Multiple feature detection */
@supports (display: grid) and (gap: 20px) {
.advanced-grid {
display: grid;
gap: 20px;
}
}
/* Nested feature queries */
@supports (display: flex) {
.flex-container {
display: flex;
@supports (gap: 1rem) {
gap: 1rem;
}
@supports not (gap: 1rem) {
margin: -0.5rem;
> * {
margin: 0.5rem;
}
}
}
}
2. CSS Variables with Fallbacks
/* CSS Custom Properties with fallbacks */
:root {
--primary-color: #007bff;
--secondary-color: #6c757d;
--spacing-unit: 1rem;
}
.button {
/* Fallback for browsers without CSS variables */
background-color: #007bff;
background-color: var(--primary-color);
padding: 0.5rem 1rem;
padding: calc(var(--spacing-unit) * 0.5) var(--spacing-unit);
}
/* Feature detection for CSS variables */
@supports (--css: variables) {
.enhanced-component {
--local-spacing: calc(var(--spacing-unit) * 2);
margin: var(--local-spacing);
}
}
/* JavaScript fallback for CSS variables */
if (!CSS.supports('--css', 'variables')) {
// Polyfill CSS variables or provide fallbacks
document.documentElement.style.setProperty('--primary-color', '#007bff');
}
3. Vendor Prefixes and Autoprefixer
/* Manual vendor prefixes (old approach) */
.animated {
-webkit-animation: slide 1s ease;
-moz-animation: slide 1s ease;
-ms-animation: slide 1s ease;
-o-animation: slide 1s ease;
animation: slide 1s ease;
}
/* Using Autoprefixer (recommended) */
// postcss.config.js
module.exports = {
plugins: [
require('autoprefixer')({
overrideBrowserslist: ['> 1%', 'last 2 versions', 'not dead']
})
]
}
// Input CSS
.example {
display: flex;
transition: all 0.3s;
user-select: none;
}
// Output CSS (after Autoprefixer)
.example {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-transition: all 0.3s;
transition: all 0.3s;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
Real-World Compatibility Patterns
Let's look at common patterns for handling compatibility in production applications:
1. Feature Detection Service
// services/featureDetection.js
class FeatureDetectionService {
constructor() {
this.features = new Map();
this.runDetection();
}
runDetection() {
// JavaScript features
this.detect('promises', () => typeof Promise !== 'undefined');
this.detect('fetch', () => 'fetch' in window);
this.detect('async', () => {
try {
eval('async () => {}');
return true;
} catch (e) {
return false;
}
});
// DOM features
this.detect('intersectionObserver', () => 'IntersectionObserver' in window);
this.detect('mutationObserver', () => 'MutationObserver' in window);
this.detect('customElements', () => 'customElements' in window);
// CSS features
this.detect('grid', () => CSS.supports('display', 'grid'));
this.detect('sticky', () => CSS.supports('position', 'sticky'));
this.detect('customProperties', () => CSS.supports('--test', '0'));
// Device features
this.detect('touch', () => 'ontouchstart' in window);
this.detect('geolocation', () => 'geolocation' in navigator);
this.detect('serviceWorker', () => 'serviceWorker' in navigator);
}
detect(feature, test) {
try {
this.features.set(feature, test());
} catch (e) {
this.features.set(feature, false);
}
}
supports(feature) {
return this.features.get(feature) || false;
}
async loadPolyfillsFor(features) {
const needed = features.filter(f => !this.supports(f));
if (needed.length === 0) return;
const polyfills = {
'promises': () => import('promise-polyfill'),
'fetch': () => import('whatwg-fetch'),
'intersectionObserver': () => import('intersection-observer'),
'customElements': () => import('@webcomponents/custom-elements')
};
const loadPromises = needed
.filter(feature => polyfills[feature])
.map(feature => polyfills[feature]());
await Promise.all(loadPromises);
}
}
export const featureDetection = new FeatureDetectionService();
2. Component Enhancement Pattern
// Progressive component enhancement
class EnhancedComponent {
constructor(element) {
this.element = element;
this.features = window.featureDetection;
// Base functionality that works everywhere
this.initBase();
// Progressive enhancements
if (this.features.supports('intersectionObserver')) {
this.addLazyLoading();
}
if (this.features.supports('customElements')) {
this.upgradeToWebComponent();
}
if (this.features.supports('grid')) {
this.element.classList.add('grid-layout');
}
}
initBase() {
// Basic functionality
this.element.addEventListener('click', this.handleClick.bind(this));
}
addLazyLoading() {
const images = this.element.querySelectorAll('img[data-src]');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
images.forEach(img => observer.observe(img));
}
upgradeToWebComponent() {
// Convert to custom element if supported
customElements.define('enhanced-component', class extends HTMLElement {
connectedCallback() {
this.innerHTML = this.element.innerHTML;
// Add shadow DOM and enhanced functionality
}
});
}
}
3. Compatibility Layer
// utils/compatibility.js
export const compat = {
// Safe event listener with passive support detection
addEventListener(element, event, handler, options = {}) {
let supportsPassive = false;
try {
const opts = Object.defineProperty({}, 'passive', {
get: () => { supportsPassive = true; }
});
window.addEventListener('testPassive', null, opts);
window.removeEventListener('testPassive', null, opts);
} catch (e) {}
const finalOptions = supportsPassive ? options : !!options.capture;
element.addEventListener(event, handler, finalOptions);
},
// Cross-browser animation frame
requestAnimationFrame(callback) {
const raf = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
(cb => setTimeout(cb, 1000 / 60));
return raf.call(window, callback);
},
// Safe querySelector with fallback
querySelector(selector, context = document) {
try {
return context.querySelector(selector);
} catch (e) {
// Fallback for older browsers
if (selector.startsWith('#')) {
return document.getElementById(selector.slice(1));
} else if (selector.startsWith('.')) {
return context.getElementsByClassName(selector.slice(1))[0];
} else {
return context.getElementsByTagName(selector)[0];
}
}
},
// Cross-browser fetch
async fetch(url, options = {}) {
if (window.fetch) {
return window.fetch(url, options);
}
// XMLHttpRequest fallback
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(options.method || 'GET', url);
Object.keys(options.headers || {}).forEach(key => {
xhr.setRequestHeader(key, options.headers[key]);
});
xhr.onload = () => {
resolve({
ok: xhr.status >= 200 && xhr.status < 300,
status: xhr.status,
json: () => Promise.resolve(JSON.parse(xhr.responseText)),
text: () => Promise.resolve(xhr.responseText)
});
};
xhr.onerror = () => reject(new Error('Network request failed'));
xhr.send(options.body);
});
}
};
Browser-Specific CSS Hacks (Use with Caution)
Sometimes you need to target specific browsers. Here are some techniques (use sparingly):
/* Internet Explorer 10-11 */
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.ie10-11 {
/* IE10-11 specific styles */
}
}
/* Edge (pre-Chromium) */
@supports (-ms-ime-align: auto) {
.edge-legacy {
/* Edge Legacy specific styles */
}
}
/* Safari only */
@media not all and (min-resolution:.001dpcm) {
@supports (-webkit-appearance:none) {
.safari-only {
/* Safari specific styles */
}
}
}
/* Firefox only */
@-moz-document url-prefix() {
.firefox-only {
/* Firefox specific styles */
}
}
/* Chrome only */
@media screen and (-webkit-min-device-pixel-ratio:0) and (min-resolution:.001dpcm) {
.chrome-only {
/* Chrome specific styles */
}
}
JavaScript Browser Detection (When Necessary)
// Modern browser detection utility
const browserDetect = {
isChrome: () => !!window.chrome && !!window.chrome.webstore,
isFirefox: () => typeof InstallTrigger !== 'undefined',
isSafari: () => /^((?!chrome|android).)*safari/i.test(navigator.userAgent),
isEdge: () => !!window.StyleMedia || !!window.MSInputMethodContext,
isIE: () => /*@cc_on!@*/false || !!document.documentMode,
// Get browser info
getBrowserInfo() {
const ua = navigator.userAgent;
let tem, M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
if (/trident/i.test(M[1])) {
tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
return { name: 'IE', version: (tem[1] || '') };
}
if (M[1] === 'Chrome') {
tem = ua.match(/\bOPR|Edge\/(\d+)/);
if (tem != null) {
return { name: 'Edge', version: tem[1] };
}
}
M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, '-?'];
if ((tem = ua.match(/version\/(\d+)/i)) != null) {
M.splice(1, 1, tem[1]);
}
return {
name: M[0],
version: M[1]
};
}
};
Performance Considerations
Compatibility code can impact performance. Here's how to optimize:
1. Conditional Loading
// Load polyfills only when needed
function loadPolyfills() {
const polyfills = [];
// Check and load what's needed
if (!window.Promise) {
polyfills.push(loadScript('/polyfills/promise.js'));
}
if (!window.fetch) {
polyfills.push(loadScript('/polyfills/fetch.js'));
}
if (!Element.prototype.closest) {
polyfills.push(loadScript('/polyfills/element-closest.js'));
}
return Promise.all(polyfills);
}
// Utility to load scripts dynamically
function loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
// Initialize app after polyfills
loadPolyfills().then(() => {
initializeApp();
});
2. Bundle Splitting
// webpack.config.js for modern/legacy bundles
module.exports = [
// Modern browsers bundle
{
entry: './src/index.js',
output: {
filename: 'modern.js',
},
target: ['web', 'es2017'],
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
targets: { esmodules: true },
useBuiltIns: 'usage',
corejs: 3
}]
]
}
}
}
]
}
},
// Legacy browsers bundle
{
entry: './src/index.js',
output: {
filename: 'legacy.js',
},
target: ['web', 'es5'],
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
targets: '> 0.25%, not dead',
useBuiltIns: 'usage',
corejs: 3
}]
]
}
}
}
]
}
}
];
// HTML differential loading
Practical Exercise: Building a Compatible Image Gallery
Let's build an image gallery that works across browsers:
// gallery.js - A progressively enhanced image gallery
class CompatibleGallery {
constructor(container) {
this.container = container;
this.images = container.querySelectorAll('img');
this.currentIndex = 0;
// Initialize with appropriate features
this.init();
}
init() {
// Basic functionality for all browsers
this.setupBasicGallery();
// Progressive enhancements
if (this.supportsIntersectionObserver()) {
this.setupLazyLoading();
}
if (this.supportsTouchEvents()) {
this.setupTouchNavigation();
}
if (this.supportsGrid()) {
this.setupGridLayout();
}
if (this.supportsCustomProperties()) {
this.setupThemeCustomization();
}
}
setupBasicGallery() {
// Create navigation buttons
const prevBtn = this.createButton('Previous', () => this.navigate(-1));
const nextBtn = this.createButton('Next', () => this.navigate(1));
this.container.appendChild(prevBtn);
this.container.appendChild(nextBtn);
// Basic keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') this.navigate(-1);
if (e.key === 'ArrowRight') this.navigate(1);
});
}
setupLazyLoading() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
if (img.dataset.src) {
img.src = img.dataset.src;
img.onload = () => img.classList.add('loaded');
}
observer.unobserve(img);
}
});
}, { threshold: 0.1 });
this.images.forEach(img => {
if (img.dataset.src) {
observer.observe(img);
}
});
}
setupTouchNavigation() {
let touchStartX = 0;
let touchEndX = 0;
this.container.addEventListener('touchstart', (e) => {
touchStartX = e.changedTouches[0].screenX;
}, false);
this.container.addEventListener('touchend', (e) => {
touchEndX = e.changedTouches[0].screenX;
this.handleSwipe();
}, false);
this.handleSwipe = () => {
const swipeThreshold = 50;
const diff = touchStartX - touchEndX;
if (Math.abs(diff) > swipeThreshold) {
if (diff > 0) {
this.navigate(1); // Swipe left, go next
} else {
this.navigate(-1); // Swipe right, go previous
}
}
};
}
setupGridLayout() {
this.container.classList.add('grid-supported');
// Add CSS through JavaScript for browsers that support grid
const style = document.createElement('style');
style.textContent = `
.grid-supported {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
`;
document.head.appendChild(style);
}
setupThemeCustomization() {
// Add theme toggle if CSS custom properties are supported
const themeToggle = this.createButton('Toggle Theme', () => {
document.body.classList.toggle('dark-theme');
});
this.container.appendChild(themeToggle);
// Add CSS variables
const style = document.createElement('style');
style.textContent = `
:root {
--gallery-bg: #ffffff;
--gallery-text: #333333;
--gallery-border: #dddddd;
}
.dark-theme {
--gallery-bg: #333333;
--gallery-text: #ffffff;
--gallery-border: #555555;
}
.gallery {
background-color: var(--gallery-bg);
color: var(--gallery-text);
border-color: var(--gallery-border);
}
`;
document.head.appendChild(style);
}
navigate(direction) {
this.currentIndex += direction;
if (this.currentIndex < 0) this.currentIndex = this.images.length - 1;
if (this.currentIndex >= this.images.length) this.currentIndex = 0;
this.showImage(this.currentIndex);
}
showImage(index) {
this.images.forEach((img, i) => {
img.style.display = i === index ? 'block' : 'none';
});
}
createButton(text, onClick) {
const button = document.createElement('button');
button.textContent = text;
button.onclick = onClick;
return button;
}
// Feature detection methods
supportsIntersectionObserver() {
return 'IntersectionObserver' in window;
}
supportsTouchEvents() {
return 'ontouchstart' in window;
}
supportsGrid() {
return CSS && CSS.supports && CSS.supports('display', 'grid');
}
supportsCustomProperties() {
return CSS && CSS.supports && CSS.supports('--test', '0');
}
}
// Usage
document.addEventListener('DOMContentLoaded', () => {
const gallery = document.querySelector('.gallery');
if (gallery) {
new CompatibleGallery(gallery);
}
});
Summary and Best Practices
Key Takeaways
- Always use feature detection instead of browser detection
- Start with progressive enhancement for better accessibility
- Use polyfills strategically - only load what's needed
- Test across multiple browsers and devices
- Consider performance impact of compatibility code
- Keep fallbacks simple and maintainable
- Use tools like Autoprefixer for CSS compatibility
- Implement differential loading for modern and legacy browsers
Compatibility Checklist
- ✓ Feature detection implemented
- ✓ Polyfills loaded conditionally
- ✓ CSS fallbacks in place
- ✓ Progressive enhancement applied
- ✓ Cross-browser testing completed
- ✓ Performance impact assessed
- ✓ Accessibility maintained across browsers
- ✓ Error handling for unsupported features
Tools and Resources
- Can I Use - Browser feature support tables
- Modernizr - Feature detection library
- Polyfill.io - Automatic polyfill service
- BrowserStack - Cross-browser testing platform
- Autoprefixer - CSS vendor prefix automation
- Babel - JavaScript transpilation
- PostCSS - CSS transformation toolkit