COMP 305: Object-Oriented Software Design

University of San Diego, Fall 2025

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:

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

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

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

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

Common Patterns

Testing Progressive Enhancement

Verify your site works at each layer:

When to Use Progressive Enhancement

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