Graceful Degradation
Graceful Degradation is a design approach that starts with a full-featured experience for modern browsers, then provides fallback experiences for older or less capable browsers. The goal is to maintain core functionality even when advanced features aren't available.
The Core Philosophy
"Build for the best experience first, then ensure it still works when features are missing."
Graceful Degradation ensures:
- Best experience for modern browsers - Take advantage of latest features
- Functional fallbacks - Core tasks still work without cutting-edge features
- Automatic adaptation - Browser capabilities detected at runtime
- User awareness - Inform users when features aren't available
Modern Feature with Multiple Fallbacks
Start with the best experience, then provide progressively simpler fallbacks.
// Modern feature with graceful degradation
class ModernVideoPlayer {
constructor(videoElement) {
this.video = videoElement;
this.setupPlayer();
}
setupPlayer() {
// Check for modern features and degrade gracefully
if (this.supportsHLS()) {
this.enableAdaptiveStreaming();
} else if (this.supportsMP4()) {
this.enableBasicPlayback();
} else {
this.showFallbackMessage();
}
// Optional enhancements
if (this.supportsPictureInPicture()) {
this.enablePiP();
}
if (this.supportsFullscreen()) {
this.enableFullscreen();
}
}
supportsHLS() {
return this.video.canPlayType('application/vnd.apple.mpegurl') !== '';
}
supportsMP4() {
return this.video.canPlayType('video/mp4') !== '';
}
supportsPictureInPicture() {
return 'pictureInPictureEnabled' in document;
}
supportsFullscreen() {
return 'requestFullscreen' in this.video;
}
enableAdaptiveStreaming() {
// Best experience: HLS adaptive streaming
this.video.src = '/video/stream.m3u8';
console.log('Adaptive streaming enabled');
}
enableBasicPlayback() {
// Fallback: Basic MP4 playback
this.video.src = '/video/video.mp4';
console.log('Basic playback enabled');
}
showFallbackMessage() {
// Last resort: Show download link
this.video.parentElement.innerHTML = `
<p>Your browser doesn't support video playback.</p>
<a href="/video/video.mp4" download>Download Video</a>
`;
}
enablePiP() {
const pipButton = document.createElement('button');
pipButton.textContent = 'Picture-in-Picture';
pipButton.onclick = () => this.video.requestPictureInPicture();
this.video.parentElement.appendChild(pipButton);
}
enableFullscreen() {
const fsButton = document.createElement('button');
fsButton.textContent = 'Fullscreen';
fsButton.onclick = () => this.video.requestFullscreen();
this.video.parentElement.appendChild(fsButton);
}
} Fallback Hierarchy
- Best - Full featured experience (modern browsers)
- Good - Core functionality with reduced features (older browsers)
- Basic - Minimal but functional experience (very old browsers)
- Message - Clear explanation when feature is unavailable
API and Storage Degradation
Modern APIs often have older equivalents. Detect and fall back gracefully.
// Graceful degradation for API features
class DataFetcher {
async getData(url) {
// Try modern fetch API
if ('fetch' in window) {
try {
const response = await fetch(url);
return await response.json();
} catch (error) {
console.warn('Fetch failed, trying XHR');
return this.getDataXHR(url);
}
}
// Fallback to XMLHttpRequest
return this.getDataXHR(url);
}
getDataXHR(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`HTTP ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error('Network error'));
xhr.send();
});
}
}
// Storage with graceful degradation
class Storage {
set(key, value) {
const data = JSON.stringify(value);
// Try localStorage (modern, persistent)
try {
localStorage.setItem(key, data);
return true;
} catch (e) {
console.warn('localStorage failed, trying sessionStorage');
}
// Try sessionStorage (fallback, session only)
try {
sessionStorage.setItem(key, data);
return true;
} catch (e) {
console.warn('sessionStorage failed, using memory');
}
// Last resort: in-memory storage (volatile)
this.memoryStore = this.memoryStore || {};
this.memoryStore[key] = data;
return true;
}
get(key) {
// Try localStorage first
try {
const data = localStorage.getItem(key);
return data ? JSON.parse(data) : null;
} catch (e) {}
// Try sessionStorage
try {
const data = sessionStorage.getItem(key);
return data ? JSON.parse(data) : null;
} catch (e) {}
// Fallback to memory
const data = this.memoryStore?.[key];
return data ? JSON.parse(data) : null;
}
} Common API Fallback Chains
- fetch → XMLHttpRequest → JSONP
- localStorage → sessionStorage → cookies → in-memory
- IndexedDB → WebSQL → localStorage
- WebSocket → Server-Sent Events → polling
- WebRTC → Flash → Plugin → "Not supported" message
CSS Graceful Degradation
Use modern CSS features with fallbacks for older browsers.
/* CSS Graceful Degradation */
/* Modern Grid layout with Flexbox fallback */
.container {
/* Flexbox fallback (older browsers) */
display: flex;
flex-wrap: wrap;
gap: 1rem; /* May not work in older browsers */
margin: -0.5rem; /* Emulate gap for old browsers */
}
.container > * {
flex: 1 1 300px;
margin: 0.5rem; /* Fallback spacing */
}
/* Modern browsers override with Grid */
@supports (display: grid) {
.container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
margin: 0; /* Reset fallback margin */
}
.container > * {
margin: 0; /* Reset fallback margin */
}
}
/* Custom Properties with fallbacks */
.button {
/* Fallback color for browsers without custom properties */
background: #0066cc;
color: white;
/* Modern: use custom properties */
background: var(--color-primary, #0066cc);
color: var(--color-text-on-primary, white);
}
/* Modern features with fallbacks */
.card {
/* Basic shadow for old browsers */
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
/* Better shadow for newer browsers */
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1),
0 2px 4px -1px rgba(0,0,0,0.06);
}
@supports (backdrop-filter: blur(10px)) {
.glass {
background: rgba(255,255,255,0.8);
backdrop-filter: blur(10px);
}
}
@supports not (backdrop-filter: blur(10px)) {
.glass {
background: rgba(255,255,255,0.95);
}
} CSS Fallback Strategies
- Declare fallback values before modern values
- Use
@supportsfor conditional application - Mobile-first approach provides natural degradation
- Test in older browsers regularly
Error Handling and Network Resilience
Handle failures gracefully with retries, timeouts, and fallback data.
// Error Handling and Fallbacks
class RobustImageLoader {
loadImage(src, fallbackSrc, errorCallback) {
const img = new Image();
img.onerror = () => {
console.warn(`Failed to load ${src}, trying fallback`);
// Try fallback image
img.onerror = () => {
console.error('Fallback image also failed');
// Show placeholder
this.showPlaceholder(img);
if (errorCallback) {
errorCallback(new Error('All image sources failed'));
}
};
img.src = fallbackSrc;
};
img.src = src;
return img;
}
showPlaceholder(img) {
// Replace with base64 placeholder
img.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjMwMCI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iI2VlZSIvPjwvc3ZnPg==';
img.alt = 'Image failed to load';
}
}
// Network resilience
class ResilientFetch {
async fetch(url, options = {}) {
const {
retries = 3,
timeout = 5000,
fallbackData = null
} = options;
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.warn(`Attempt ${attempt} failed:`, error.message);
if (attempt === retries) {
// All retries exhausted
if (fallbackData !== null) {
console.log('Using fallback data');
return fallbackData;
}
throw error;
}
// Wait before retry (exponential backoff)
await this.sleep(Math.pow(2, attempt) * 1000);
}
}
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
} Offline Support
Degrade gracefully when network connection is lost. Queue actions and sync when reconnected.
// Offline Support with Graceful Degradation
class OfflineCapableApp {
constructor() {
this.checkOnlineStatus();
this.setupEventListeners();
this.registerServiceWorker();
}
checkOnlineStatus() {
if (!navigator.onLine) {
this.enableOfflineMode();
}
}
setupEventListeners() {
window.addEventListener('online', () => {
this.enableOnlineMode();
this.syncPendingData();
});
window.addEventListener('offline', () => {
this.enableOfflineMode();
});
}
enableOfflineMode() {
document.body.classList.add('offline');
this.showOfflineBanner();
// Disable features that require connection
this.disableRealTimeFeatures();
// Enable offline features
this.enableLocalCaching();
this.queuePendingActions();
}
enableOnlineMode() {
document.body.classList.remove('offline');
this.hideOfflineBanner();
// Re-enable online features
this.enableRealTimeFeatures();
this.clearLocalCache();
}
showOfflineBanner() {
const banner = document.createElement('div');
banner.className = 'offline-banner';
banner.textContent = 'You are offline. Some features are unavailable.';
document.body.prepend(banner);
}
hideOfflineBanner() {
document.querySelector('.offline-banner')?.remove();
}
async registerServiceWorker() {
if ('serviceWorker' in navigator) {
try {
await navigator.serviceWorker.register('/sw.js');
console.log('Service Worker registered for offline support');
} catch (error) {
console.warn('Service Worker registration failed:', error);
// App still works, just without offline caching
}
} else {
console.log('Service Workers not supported - offline features disabled');
}
}
disableRealTimeFeatures() {
// Close WebSocket connections
// Disable live updates
// Show cached data
}
enableRealTimeFeatures() {
// Reconnect WebSockets
// Enable live updates
// Fetch fresh data
}
enableLocalCaching() {
// Cache API responses locally
// Store user actions for later
}
queuePendingActions() {
// Queue writes for when online
}
async syncPendingData() {
// Upload queued actions
// Sync with server
console.log('Syncing data after reconnection...');
}
clearLocalCache() {
// Clean up temporary offline data
}
} Offline Strategies
- Detect online/offline status
- Show clear indicators when offline
- Cache content for offline viewing
- Queue user actions for later sync
- Provide offline-capable features
Comprehensive Feature Detection
Detect browser capabilities and adapt functionality accordingly.
// Comprehensive Feature Detection and Fallbacks
class FeatureManager {
constructor() {
this.features = {
// Storage
localStorage: this.testLocalStorage(),
indexedDB: 'indexedDB' in window,
// Network
fetch: 'fetch' in window,
serviceWorker: 'serviceWorker' in navigator,
// Media
webRTC: 'RTCPeerConnection' in window,
mediaRecorder: 'MediaRecorder' in window,
// Graphics
webGL: this.testWebGL(),
canvas: this.testCanvas(),
// Sensors
geolocation: 'geolocation' in navigator,
deviceOrientation: 'DeviceOrientationEvent' in window,
// Performance
intersectionObserver: 'IntersectionObserver' in window,
performanceObserver: 'PerformanceObserver' in window,
// Misc
notifications: 'Notification' in window,
vibration: 'vibrate' in navigator
};
this.logCapabilities();
}
testLocalStorage() {
try {
localStorage.setItem('test', 'test');
localStorage.removeItem('test');
return true;
} catch (e) {
return false;
}
}
testWebGL() {
try {
const canvas = document.createElement('canvas');
return !!(canvas.getContext('webgl') || canvas.getContext('experimental-webgl'));
} catch (e) {
return false;
}
}
testCanvas() {
const canvas = document.createElement('canvas');
return !!(canvas.getContext && canvas.getContext('2d'));
}
logCapabilities() {
console.group('Browser Capabilities');
Object.entries(this.features).forEach(([feature, supported]) => {
console.log(`${feature}: ${supported ? '✓' : '✗'}`);
});
console.groupEnd();
}
has(feature) {
return this.features[feature] || false;
}
requires(features) {
return features.every(feature => this.has(feature));
}
}
// Usage
const features = new FeatureManager();
if (features.has('fetch')) {
// Use modern fetch
} else {
// Fall back to XHR
}
if (features.requires(['geolocation', 'notifications'])) {
// Enable location-based notifications
} Benefits
- Modern Experience - Leverage cutting-edge features when available
- Wider Compatibility - Works in older browsers with reduced features
- Future-Ready - Easy to adopt new features as they become available
- User Awareness - Users understand when features aren't supported
- Practical Development - Build for modern browsers, add fallbacks as needed
Best Practices
- Feature Detection - Never use browser sniffing
- Fallback Testing - Test degraded experiences regularly
- Clear Communication - Tell users why features aren't available
- Graceful Failures - Never show error screens for missing features
- Progressive Baseline - Define minimum requirements clearly
- Polyfills - Use judiciously for critical features
When to Use Graceful Degradation
- Cutting-edge features - Want to use newest APIs immediately
- Known audience - Most users on modern browsers
- Nice-to-have features - Enhanced experiences that aren't critical
- Experimental features - Testing new capabilities
- Performance optimizations - Modern features provide better performance
Graceful Degradation vs Progressive Enhancement
| Graceful Degradation | Progressive Enhancement |
|---|---|
| Start with full experience | Start with baseline |
| Degrade when features missing | Enhance when features available |
| Modern browser focus | Universal compatibility focus |
| Add fallbacks after | Add enhancements on top |
Best Approach: Use both! Progressive Enhancement for core functionality, Graceful Degradation for advanced features.
Testing Degradation
Verify fallbacks work correctly:
- Disable features - Use browser dev tools to disable features
- Old browsers - Test in IE11, older Safari, older mobile browsers
- Simulated failures - Test with network throttling, offline mode
- Feature flags - Toggle features on/off programmatically
- Automated testing - Test fallback paths in your test suite