Progressive enhancement ensures your web app works for all users, regardless of their browser or device capabilities.
Core Principles
- 1Start with Basic HTML: Content must be accessible without JavaScript
- 2Layer on CSS: Enhance presentation and layout
- 3Add JavaScript: Enhance functionality progressively
- 4Test Extremes: Ensure functionality in minimal environments
Layer 1: Semantic HTML
Content-First Approach
html
<form action="/submit" method="POST">
<label for="email">Email:</label>
<input id="email" name="email" type="email" required>
<button type="submit">Subscribe</button>
</form>This works without JavaScript!
Semantic Elements
Use proper HTML5 elements:
<nav>for navigation<article>for self-contained content<section>for thematic grouping<aside>for tangentially related content
Layer 2: CSS Enhancement
Mobile-First Base Styles
css
/* Base styles for all */
.container {
width: 100%;
padding: 1rem;
}
/* Enhanced for larger screens */
@media (min-width: 768px) {
.container {
max-width: 1200px;
margin: 0 auto;
}
}Feature Queries
Use CSS features when available:
css
@supports (display: grid) {
.grid {
display: grid;
}
}
@supports not (display: grid) {
.grid {
display: flex;
flex-wrap: wrap;
}
}CSS Custom Properties with Fallbacks
css
.button {
background: #0066cc; /* Fallback */
background: var(--button-color, #0066cc);
}Layer 3: JavaScript Enhancement
Feature Detection
Before using new APIs:
javascript
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver(callback);
// Use observer
} else {
// Fallback: load all images immediately
}Progressive Form Enhancement
javascript
// Base: HTML form works without JS
const form = document.querySelector('form');
// Enhance: AJAX submission
if ('fetch' in window) {
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
const response = await fetch(form.action, {
method: 'POST',
body: formData
});
// Handle response
showSuccessMessage();
});
}Lazy Loading Enhancements
javascript
// Base: Images load normally
<img src="photo.jpg" alt="Photo">
// Enhanced: Lazy load when supported
if ('IntersectionObserver' in window) {
const images = document.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));
}Practical Examples
Responsive Images
html
<!-- Base: Single image -->
<img src="image-small.jpg" alt="Description">
<!-- Progressive: Multiple sources -->
<picture>
<source srcset="image-large.webp" type="image/webp" media="(min-width: 1024px)">
<source srcset="image-medium.webp" type="image/webp" media="(min-width: 768px)">
<source srcset="image-small.jpg">
<img src="image-small.jpg" alt="Description" loading="lazy">
</picture>Offline Support
javascript
// Base: App works online
// Enhanced: Cache for offline access
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW registered'))
.catch(err => console.log('SW failed'));
});
}Geolocation Enhancement
javascript
// Base: Manual location input
<input type="text" name="location" placeholder="Enter your city">
// Enhanced: Auto-detect location
if ('geolocation' in navigator) {
const button = document.createElement('button');
button.textContent = 'Use my location';
button.addEventListener('click', () => {
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords;
// Reverse geocode to get city
updateLocationField(latitude, longitude);
},
(error) => {
showError('Could not get location');
}
);
});
form.appendChild(button);
}Testing Progressive Enhancement
Test Strategy
- 1Disable JavaScript and verify core functionality
- 2Test on slow networks (Chrome DevTools)
- 3Use older browsers via BrowserStack
- 4Test with screen readers
- 5Validate with WAVE axe DevTools
Lighthouse Testing
Target scores:
- Performance: 90+
- Accessibility: 100
- Best Practices: 90+
- SEO: 90+
Benefits
- 1Broader Audience: Works for everyone
- 2Better SEO: Search engines crawl content
- 3Faster Perceived Performance: Content loads quickly
- 4Graceful Degradation: Fallbacks for unsupported features
- 5Future-Proof: New browsers get enhancements
Conclusion
Progressive enhancement isn't about limiting features—it's about ensuring everyone gets a functional experience. Start with solid HTML/CSS, then enhance with JavaScript. Test thoroughly, and build apps that work everywhere.