Progressive Enhancement
Progressive Enhancement is a design philosophy that emphasizes core functionality first, then progressively adds enhanced experiences for more capable browsers and devices. Start with a baseline experience that works for everyone, then layer on improvements.
The Core Philosophy
"Basic content should be accessible to all web browsers. Enhanced layout, styling, and behavior should be added through separate layers of CSS and JavaScript."
Progressive Enhancement ensures your application:
- Works for everyone - Core functionality accessible to all users
- Fails gracefully - Degrades smoothly when features aren't supported
- Performs better - Lighter baseline, enhancements load progressively
- Future-proof - New browsers get enhancements automatically
Layer 1: Semantic HTML Foundation
Start with semantic HTML that works without CSS or JavaScript. Forms submit, links navigate, content is accessible.
<!-- LAYER 1: Semantic HTML (works everywhere) -->
<form action="/search" method="GET" class="search-form">
<label for="query">Search:</label>
<input type="search"
id="query"
name="q"
required
placeholder="Enter search term...">
<button type="submit">Search</button>
<output id="results" aria-live="polite">
<!-- Server-side results rendered here -->
</output>
</form>
<!-- Progressive enhancement principles:
1. Works without CSS
2. Works without JavaScript
3. Submits to server and reloads page
--> Baseline Requirements
- Valid, semantic HTML5
- Functional without CSS or JavaScript
- Accessible to screen readers
- Works in text-only browsers
- Server-side rendering for content
Layer 2: CSS Enhancement
Add visual design that enhances but doesn't break the experience. Use feature queries for progressive CSS.
/* LAYER 2: CSS Enhancement (better visual experience) */
.search-form {
display: flex;
gap: 0.5rem;
align-items: center;
padding: 1rem;
background: var(--color-bg-secondary);
border-radius: 8px;
}
.search-form input {
flex: 1;
padding: 0.5rem;
border: 1px solid var(--color-border);
border-radius: 4px;
}
.search-form button {
padding: 0.5rem 1rem;
background: var(--color-primary);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
}
.search-form button:hover {
background: var(--color-secondary);
}
/* Still works if CSS fails to load - just less pretty */
/* Enhanced features for capable browsers */
@supports (display: grid) {
.search-results {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
} CSS Best Practices
- Use
@supportsfor feature detection - Mobile-first responsive design
- Respect user preferences (
prefers-reduced-motion,prefers-color-scheme) - Graceful fallbacks for newer properties
Layer 3: JavaScript Enhancement
Enhance interactivity with JavaScript, but ensure the site works without it. Use feature detection, not browser detection.
/* LAYER 3: JavaScript Enhancement (interactive experience) */
// Feature detection before enhancement
if ('fetch' in window && 'FormData' in window) {
const searchForm = document.querySelector('.search-form');
const resultsOutput = document.getElementById('results');
// Enhance form with AJAX submission
searchForm.addEventListener('submit', async (e) => {
e.preventDefault(); // Progressive: only prevent if JS works
const formData = new FormData(searchForm);
const query = formData.get('q');
try {
// Show loading state
resultsOutput.textContent = 'Searching...';
// Fetch results without page reload
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const results = await response.json();
// Update UI dynamically
displayResults(results);
// Update URL without reload
history.pushState({ query }, '', `/search?q=${encodeURIComponent(query)}`);
} catch (error) {
// Fallback: if AJAX fails, fall back to regular form submission
console.error('Search failed, falling back to standard form submission');
searchForm.submit();
}
});
function displayResults(results) {
if (results.length === 0) {
resultsOutput.innerHTML = '<p>No results found.</p>';
return;
}
resultsOutput.innerHTML = results.map(result => `
<article class="search-result">
<h3>${escapeHTML(result.title)}</h3>
<p>${escapeHTML(result.snippet)}</p>
<a href="${escapeHTML(result.url)}">Read more</a>
</article>
`).join('');
}
function escapeHTML(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
}
// If fetch isn't supported, form still works via standard submission JavaScript Principles
- Detect features before using them
- Gracefully fall back to server-side behavior
- Handle errors and network failures
- Don't break the back button or bookmarks
- Preserve accessibility in enhanced experiences
Layered Enhancement Strategy
Build capabilities progressively based on what the browser supports.
// Progressive Enhancement: Build in Layers
class ProgressiveSearchWidget {
constructor(formElement) {
this.form = formElement;
this.capabilities = this.detectCapabilities();
this.enhance();
}
detectCapabilities() {
return {
fetch: 'fetch' in window,
formData: 'FormData' in window,
history: 'pushState' in history,
intersectionObserver: 'IntersectionObserver' in window,
serviceWorker: 'serviceWorker' in navigator
};
}
enhance() {
// Layer 1: Basic HTML - already works, do nothing
// Layer 2: Enhance with AJAX if supported
if (this.capabilities.fetch && this.capabilities.formData) {
this.enableAjaxSearch();
}
// Layer 3: Add URL management if supported
if (this.capabilities.history) {
this.enableHistoryManagement();
}
// Layer 4: Add instant search if supported
if (this.capabilities.intersectionObserver) {
this.enableInstantSearch();
}
// Layer 5: Add offline support if supported
if (this.capabilities.serviceWorker) {
this.enableOfflineSearch();
}
}
enableAjaxSearch() {
this.form.addEventListener('submit', this.handleAjaxSubmit.bind(this));
}
async handleAjaxSubmit(e) {
e.preventDefault();
try {
const formData = new FormData(this.form);
const response = await fetch(this.form.action + '?' + new URLSearchParams(formData));
const results = await response.json();
this.displayResults(results);
} catch (error) {
// Fallback to standard submission
this.form.submit();
}
}
enableHistoryManagement() {
window.addEventListener('popstate', (e) => {
if (e.state?.query) {
this.performSearch(e.state.query);
}
});
}
enableInstantSearch() {
const input = this.form.querySelector('input[type="search"]');
let debounceTimer;
input.addEventListener('input', (e) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
if (e.target.value.length >= 3) {
this.performSearch(e.target.value);
}
}, 300);
});
}
async enableOfflineSearch() {
if (await this.isOffline()) {
this.showOfflineMessage();
}
window.addEventListener('online', () => this.hideOfflineMessage());
window.addEventListener('offline', () => this.showOfflineMessage());
}
async isOffline() {
return !navigator.onLine;
}
displayResults(results) {
// Implementation
}
performSearch(query) {
// Implementation
}
showOfflineMessage() {
// Implementation
}
hideOfflineMessage() {
// Implementation
}
} Feature Detection
Always test for feature support, never assume based on browser name or version.
// Feature Detection (not browser detection)
// GOOD: Feature detection
if ('geolocation' in navigator) {
navigator.geolocation.getCurrentPosition(success, error);
}
// BAD: Browser detection
if (navigator.userAgent.includes('Chrome')) {
// Don't do this!
}
// GOOD: Test the feature
const supportsWebP = () => {
const canvas = document.createElement('canvas');
return canvas.toDataURL('image/webp').startsWith('data:image/webp');
};
// GOOD: CSS feature queries
@supports (display: grid) {
.container {
display: grid;
}
}
@supports not (display: grid) {
.container {
display: flex; /* Fallback */
}
}
// GOOD: Graceful enhancement
class ImageUploader {
constructor() {
this.input = document.querySelector('input[type="file"]');
// Start with basic file upload (works everywhere)
// Then progressively enhance
if ('FileReader' in window) {
this.enablePreview();
}
if ('FormData' in window && 'fetch' in window) {
this.enableAjaxUpload();
}
if ('Blob' in window) {
this.enableImageCompression();
}
}
enablePreview() {
this.input.addEventListener('change', (e) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (e) => {
this.showPreview(e.target.result);
};
reader.readAsDataURL(file);
});
}
enableAjaxUpload() {
// AJAX upload without page reload
}
enableImageCompression() {
// Compress before upload
}
} Responsive Images
Progressive enhancement applies to media too. Start simple, add responsive images, modern formats, and lazy loading.
<!-- Progressive Enhancement for Images -->
<!-- Layer 1: Basic image (works everywhere) -->
<img src="/images/photo-800w.jpg"
alt="Mountain landscape">
<!-- Layer 2: Responsive images for better performance -->
<img src="/images/photo-800w.jpg"
srcset="/images/photo-400w.jpg 400w,
/images/photo-800w.jpg 800w,
/images/photo-1200w.jpg 1200w"
sizes="(max-width: 600px) 400px,
(max-width: 1000px) 800px,
1200px"
alt="Mountain landscape">
<!-- Layer 3: Modern formats with fallbacks -->
<picture>
<source srcset="/images/photo.avif" type="image/avif">
<source srcset="/images/photo.webp" type="image/webp">
<img src="/images/photo.jpg" alt="Mountain landscape">
</picture>
<!-- Layer 4: Lazy loading for performance -->
<img src="/images/photo.jpg"
loading="lazy"
decoding="async"
alt="Mountain landscape"> Benefits
- Universal Access - Works on any device, browser, or connection
- Better Performance - Core content loads fast, enhancements load progressively
- Improved SEO - Search engines can crawl server-rendered content
- Resilience - Site remains functional when JavaScript fails or is blocked
- Accessibility - Baseline HTML is naturally more accessible
- Maintainability - Clear separation between core and enhanced features
Common Patterns
- Forms - Server-side submission → AJAX → real-time validation
- Navigation - Full page loads → AJAX navigation → SPA routing
- Images - Standard img → srcset/sizes → WebP/AVIF → lazy loading
- Video - YouTube embed → HTML5 video → adaptive streaming
- Comments - Server-rendered → AJAX loading → real-time updates
Testing Progressive Enhancement
Verify your site works at each layer:
- Disable JavaScript - Does the site still function?
- Disable CSS - Is content still readable and logical?
- Slow connection - Does it work on 3G? 2G?
- Screen reader - Can users navigate and complete tasks?
- Keyboard only - Can you use the site without a mouse?
- Old browsers - Test in IE11 or older Safari versions
When to Use Progressive Enhancement
- Public-facing websites - Maximize reach and accessibility
- E-commerce - Don't lose sales due to browser incompatibility
- Content sites - Ensure search engines can index everything
- Global audience - Support diverse devices and connections
- Government/civic sites - Legal accessibility requirements
Contrast with Graceful Degradation
Progressive Enhancement and Graceful Degradation are related but different:
| Progressive Enhancement | Graceful Degradation |
|---|---|
| Start with baseline, add features | Start with full experience, remove features |
| Core functionality works everywhere | Best experience for modern browsers |
| Design for constraints first | Design for ideal case first |