Skip to content

Mobile-First Progressive Enhancement — Complete Guide

DodaTech Updated 2026-06-28 10 min read

In this tutorial, you will learn about Mobile. We cover key concepts, practical examples, and best practices to help you master this topic.

Mobile-first progressive enhancement builds core content and functionality for all browsers, then layers enhanced features using feature detection and graceful enhancement for capable devices.

What You'll Learn

  • Progressive enhancement vs graceful degradation
  • Feature detection techniques
  • HTML-first content strategy
  • CSS enhancement layers
  • JavaScript enhancement layers
  • Network-aware enhancements
  • Capability detection for modern APIs

Why It Matters

  • Not all mobile browsers support modern features
  • JavaScript failures should not break core content
  • Feature detection prevents errors on old browsers
  • Progressive enhancement ensures universal access

Real-World Use

  • A form that works with HTML validation alone, enhanced with JS validation
  • An image gallery that shows a static grid, enhanced with lightbox
  • A map that shows a static location image, enhanced with interactive map
  • A video player that shows a poster image, enhanced with native playback
flowchart LR
  A[Progressive Enhancement] --> B[HTML Core]
  A --> C[CSS Enhancements]
  A --> D[JS Enhancements]
  A --> E[Network Aware]
  B --> F[Content + semantics]
  C --> G[Layout + styling]
  D --> H[Interactivity]
  E --> I[Conditional loading]

HTML-First Core

Start with semantic HTML that works without CSS or JavaScript. The core content must be accessible and functional at the most basic level.

Code Example: HTML-First Form

<!-- Core: works without CSS or JavaScript -->
<form action="/submit" method="POST" class="progressive-form">
    <fieldset>
        <legend>Contact Information</legend>

        <label for="pe-name">Full Name</label>
        <input type="text" id="pe-name" name="name" required autocomplete="name">

        <label for="pe-email">Email Address</label>
        <input type="email" id="pe-email" name="email" required autocomplete="email">

        <label for="pe-message">Message</label>
        <textarea id="pe-message" name="message" required rows="4"></textarea>
    </fieldset>

    <button type="submit">Send Message</button>
</form>

<!-- Enhancement layer 1: CSS styling (loaded via CSS) -->
<!-- Enhancement layer 2: JS validation (loaded via JS) -->

<script>
// Enhancement: JavaScript validation only if available
if (document.querySelector) {
    const form = document.querySelector('.progressive-form');
    if (form) {
        form.addEventListener('submit', function(e) {
            const email = document.getElementById('pe-email');
            if (email && email.value && !email.value.includes('@')) {
                e.preventDefault();
                alert('Please enter a valid email address');
                email.focus();
            }
        });
    }
}

// Enhancement: Character count on textarea
if ('IntersectionObserver' in window && document.querySelector) {
    const textarea = document.getElementById('pe-message');
    if (textarea) {
        const counter = document.createElement('span');
        counter.className = 'char-count';
        textarea.parentNode.insertBefore(counter, textarea.nextSibling);

        textarea.addEventListener('input', function() {
            counter.textContent = `${this.value.length} / 500 characters`;
        });
    }
}
</script>

Expected output: The form works without CSS or JavaScript: labels are connected to inputs, required fields prevent empty submission, native browser validation handles email format. JavaScript adds character count and enhanced validation as an additional layer.

CSS Enhancement Layers

Layer CSS from basic to advanced, ensuring readable content at every level.

Code Example: CSS Layering

/* Layer 1: Core — semantic, readable */
/* Already works: browser defaults provide readable text */

/* Layer 2: Basic layout — single column, readable */
.progressive-form {
    max-width: 480px;
    margin: 2rem auto;
    padding: 1rem;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.progressive-form label {
    display: block;
    margin-top: 1rem;
    font-weight: 600;
}

.progressive-form input,
.progressive-form textarea {
    display: block;
    width: 100%;
    padding: 0.5rem;
    margin-top: 0.25rem;
    border: 1px solid #ccc;
    font-size: 1rem;
    font-family: inherit;
}

.progressive-form button {
    margin-top: 1rem;
    padding: 0.75rem 1.5rem;
    font-size: 1rem;
    background: #3b82f6;
    color: #fff;
    border: none;
    cursor: pointer;
}

/* Layer 3: Enhanced — if @supports passes */
@supports (display: grid) {
    .progressive-form fieldset {
        display: grid;
        gap: 1rem;
    }
}

@supports (border-radius: 8px) {
    .progressive-form input,
    .progressive-form textarea {
        border-radius: 6px;
    }
}

@supports (aspect-ratio: 1) {
    .progressive-form input[type="checkbox"] {
        aspect-ratio: 1;
        width: 20px;
        height: 20px;
    }
}

/* Layer 4: Feature-specific — custom properties */
@supports (--custom: property) {
    .progressive-form {
        --primary: #3b82f6;
        --border: #d1d5db;
    }

    .progressive-form input:focus {
        border-color: var(--primary);
        box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
    }
}

Expected output: On a very old browser, the form renders with default browser styles (readable, functional). On a modern browser, CSS Grid, border-radius, aspect-ratio, and custom properties enhance the appearance and layout.

JavaScript Enhancement with Feature Detection

Use feature detection to add enhancements only when the browser supports the required APIs.

Code Example: Feature Detection

// Feature detection utilities
const features = {
    // Core APIs
    intersectionObserver: 'IntersectionObserver' in window,
    resizeObserver: 'ResizeObserver' in window,
    mutationObserver: 'MutationObserver' in window,

    // Network
    fetch: 'fetch' in window,
    serviceWorker: 'serviceWorker' in navigator,
    beacon: 'sendBeacon' in navigator,

    // Storage
    localStorage: (() => {
        try { localStorage.setItem('test', '1'); localStorage.removeItem('test'); return true; }
        catch (e) { return false; }
    })(),
    sessionStorage: (() => {
        try { sessionStorage.setItem('test', '1'); sessionStorage.removeItem('test'); return true; }
        catch (e) { return false; }
    })(),

    // Media
    webP: (() => {
        const canvas = document.createElement('canvas');
        return canvas.toDataURL('image/webp').startsWith('data:image/webp');
    })(),
    avif: (() => {
        // Check AVIF support via picture element
        const picture = document.createElement('picture');
        const source = document.createElement('source');
        source.type = 'image/avif';
        picture.appendChild(source);
        return source.canPlayType && source.canPlayType('image/avif') !== '';
    })(),

    // CSS features
    cssGrid: CSS.supports('display', 'grid'),
    cssCustomProperties: CSS.supports('--custom', 'property'),
    cssAspectRatio: CSS.supports('aspect-ratio', '1'),
    cssContainerQueries: CSS.supports('container-type', 'inline-size'),

    // Input
    touchScreen: 'ontouchstart' in window || navigator.maxTouchPoints > 0,
    darkMode: window.matchMedia('(prefers-color-scheme: dark)').matches,
    reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches,

    // Device
    webGL: (() => {
        try {
            const canvas = document.createElement('canvas');
            return !!canvas.getContext('webgl') || !!canvas.getContext('experimental-webgl');
        } catch (e) { return false; }
    })(),
    webWorker: 'Worker' in window,
    webSocket: 'WebSocket' in window
};

// Conditional enhancement loader
class ProgressiveEnhancer {
    constructor() {
        this.loaded = new Set();
    }

    load(feature, url, fallback) {
        if (features[feature]) {
            const script = document.createElement('script');
            script.src = url;
            script.async = true;
            script.onload = () => this.loaded.add(feature);
            document.body.appendChild(script);
        } else if (fallback) {
            fallback();
        }
    }

    init() {
        // Load lightbox only if IntersectionObserver and fetch are available
        this.load('intersectionObserver', '/js/lightbox.js', () => {
            // Fallback: add download links instead
            document.querySelectorAll('[data-lightbox]').forEach(el => {
                const link = document.createElement('a');
                link.href = el.dataset.src;
                link.textContent = 'View full image';
                el.parentNode.appendChild(link);
            });
        });

        // Load offline support only if serviceWorker is available
        if (features.serviceWorker) {
            navigator.serviceWorker.register('/sw.js');
        }

        // Load analytics only if beacon and localStorage are available
        if (features.beacon && features.localStorage) {
            const analytics = document.createElement('script');
            analytics.src = '/js/analytics.js';
            analytics.async = true;
            document.body.appendChild(analytics);
        }

        // Apply dark mode CSS class
        if (features.darkMode) {
            document.documentElement.classList.add('dark-mode');
        }

        // Apply reduced motion class
        if (features.reducedMotion) {
            document.documentElement.classList.add('reduced-motion');
        }
    }
}

// Usage
const enhancer = new ProgressiveEnhancer();
enhancer.init();

Expected output: The script detects browser capabilities and loads enhanced features only when supported. If IntersectionObserver is missing, a fallback adds direct download links instead of a lightbox. Analytics only loads if sendBeacon and localStorage are available. Dark mode and reduced motion preferences are applied immediately.

Network-Aware Enhancement

Use the Network Information API to adjust loading behavior based on connection speed.

Code Example: Network-Aware Loading

// Network-aware progressive enhancement
class NetworkAwareLoader {
    constructor() {
        this.connection = navigator.connection || null;
        this.setup();
    }

    get networkType() {
        if (!this.connection) return 'unknown';
        return this.connection.effectiveType; // 'slow-2g', '2g', '3g', '4g'
    }

    get isSlowConnection() {
        return ['slow-2g', '2g'].includes(this.networkType);
    }

    get isSaveData() {
        return this.connection?.saveData || false;
    }

    setup() {
        // Listen for connection changes
        if (this.connection) {
            this.connection.addEventListener('change', () => {
                this.adjustForNetwork();
            });
        }

        // Initial adjustment
        this.adjustForNetwork();
    }

    adjustForNetwork() {
        document.documentElement.dataset.network = this.networkType;

        if (this.isSaveData) {
            document.documentElement.classList.add('save-data');
        }

        if (this.isSlowConnection) {
            // Replace videos with poster images
            document.querySelectorAll('video').forEach(video => {
                video.pause();
                video.removeAttribute('src');
                video.innerHTML = `<img src="${video.poster}" alt="${video.alt || 'Video thumbnail'}" loading="lazy">`;
            });

            // Load low-res images
            document.querySelectorAll('img[data-src-high]').forEach(img => {
                img.src = img.dataset.srcLow || img.src;
            });

            // Defer non-critical CSS
            document.querySelectorAll('link[data-defer]').forEach(link => {
                // Already deferred; keep it that way
            });

            // Disable animations
            document.documentElement.classList.add('reduced-motion');
        }
    }

    // Dynamic loading based on network
    async loadImage(src, lowResSrc = null) {
        if (this.isSlowConnection && lowResSrc) {
            return lowResSrc;
        }
        return src;
    }

    async loadVideo(posterSrc, videoSrc) {
        if (this.isSlowConnection) {
            return { type: 'image', src: posterSrc };
        }
        return { type: 'video', src: videoSrc };
    }

    shouldPreload() {
        return !this.isSlowConnection && !this.isSaveData;
    }

    shouldLazyLoad() {
        return this.isSlowConnection || this.isSaveData;
    }
}

// Usage
const loader = new NetworkAwareLoader();

// Image loading
document.querySelectorAll('img[data-src]').forEach(async (img) => {
    const highRes = img.dataset.src;
    const lowRes = img.dataset.srcLow || null;
    img.src = await loader.loadImage(highRes, lowRes);
});

// Video loading
document.querySelectorAll('video[data-src]').forEach(async (video) => {
    const result = await loader.loadVideo(video.poster, video.dataset.src);
    if (result.type === 'image') {
        const img = document.createElement('img');
        img.src = result.src;
        img.alt = video.alt || '';
        img.loading = 'lazy';
        video.parentNode.replaceChild(img, video);
    }
});

Expected output: On fast connections (4G/WiFi), all assets load normally. On slow connections (2G/slow-3G) or when save-data is enabled, videos are replaced with poster images, low-resolution images load instead of high-res, animations are disabled, and non-critical CSS is deferred.

Common Mistakes

  1. Graceful degradation instead of progressive enhancement — Building for modern browsers first and then fixing for old browsers (degradation) is harder than starting with a solid core and enhancing.
  2. No feature detection before using APIs — Using fetch, localStorage, or IntersectionObserver without checking availability causes runtime errors in older browsers.
  3. JavaScript-dependent core content — If JavaScript fails to load, users see a blank page. Core content must render with HTML alone.
  4. CSS-only interactivity without HTML fallback — Accordions and tabs that use :target or checkbox hacks work in CSS but fail if CSS does not load. Provide HTML-only fallback content.
  5. Blocking enhancement on feature detection — Even if a feature is detected, the network or device may be too slow to use it. Add timeouts for enhancement loading.
  6. Not testing without JavaScript — Many users browse with JavaScript disabled or blocked by corporate firewalls. Test core functionality with JS off.
  7. Ignoring network-aware loading — Loading 4K images on a 2G connection wastes the user's data and time. Use Network Information API to adapt.

Practice Questions

  1. What is the difference between progressive enhancement and graceful degradation? Progressive enhancement starts with a basic functional core and adds enhancements for capable browsers. Graceful degradation builds for modern browsers and tries to fix for older ones.
  2. What is the @supports CSS rule used for? CSS feature detection. @supports (display: grid) applies styles only when the browser supports CSS Grid.
  3. How do you detect localStorage availability? Try writing and reading a test value in a try/catch block. Some browsers disable localStorage in private mode.
  4. What does the Network Information API provide? navigator.connection gives the user's effective network type (4G, 3G, 2G), downlink speed, round-trip time, and save-data preference.
  5. Why should forms work without JavaScript? JavaScript may fail to load or execute due to network errors, ad blockers, corporate policies, or browser extensions. HTML form validation and server-side handling ensure the form always works.

Challenge

Build a progressively enhanced image gallery page. Core (no CSS, no JS): a list of links to full-resolution images with descriptive text. CSS layer: a responsive grid layout with thumbnails and hover effects. JavaScript layer 1: Lazy Loading with IntersectionObserver and WebP detection (serve WebP if supported). JavaScript Layer 2: a lightbox modal with swipe support, keyboard navigation, and pinch-to-zoom. Network-aware: on slow connections, serve lower resolution images and disable the lightbox (replace with direct download links). Feature detection: check for each enhancement before loading the associated script.

FAQ

Is progressive enhancement still relevant in 2026?

Yes. While modern browsers have converged, users still access the web on old devices, with JavaScript disabled, on slow networks, or with assistive technologies. Progressive enhancement ensures everyone gets a functional experience.

How do I test progressive enhancement?

Use Chrome DevTools to disable CSS and JavaScript separately. Use the Coverage tab to verify critical CSS/JS is minimal. Use WebPageTest with different browser profiles. Test on actual old devices.

What is the cut-off browser for progressive enhancement?

Support browsers with at least 1% global usage. For mobile, this typically means the last 2-3 major versions of Chrome, Safari, and Samsung Internet. Use feature detection rather than browser sniffing.

How do I handle JavaScript failures gracefully?

Use the HTML noscript element to show fallback content. Use CSS .no-js patterns to hide enhanced elements when JS is not available. Use network timeouts for dynamic script loading.

What is the performance cost of progressive enhancement?

The core HTML-first approach adds very little overhead (semantic markup is free). Feature detection scripts are typically under 1KB. Conditional loading adds network requests only for supported features.

Mini Project

Build a progressively enhanced media page with: (1) core HTML content (article text, video with poster and fallback link, image gallery with links), (2) CSS enhancement (responsive layout, typography, dark mode support via prefers-color-scheme), (3) JavaScript enhancement (IntersectionObserver lazy loading, WebP detection and serving, lightbox gallery with keyboard support, video player with custom controls), (4) network-aware loading (replace videos with poster images on slow connections, serve compressed images on save-data mode), (5) feature detection for each API before use, (6) graceful fallbacks when features are missing (no lightbox = direct download links, no lazy loading = regular img tags, no fetch = form submission), (7) test the page with CSS off, JS off, and on throttled 2G.

What's Next

Continue with Lesson 20: Mobile-First Checklist to review everything covered in this course with a comprehensive checklist.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro