Skip to content

Progressive Enhancement in MPAs — Building Without JavaScript Dependency

DodaTech Updated 2026-06-28 7 min read

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

Progressive enhancement in MPAs builds core functionality with HTML first, then layers CSS for presentation and JavaScript for enhancement, ensuring the application works without any client-side scripting.

What You'll Learn

By the end of this tutorial, you will understand the progressive enhancement philosophy, how to build MPAs that work without JavaScript, how to layer enhancements using feature detection, how to handle forms and navigation without JS dependency, and how testing ensures core functionality works in all environments.

Why It Matters

Not all users have JavaScript enabled. Some use assistive technologies that do not execute JavaScript. Others are on slow networks where JS fails to load. Progressive enhancement ensures your MPA is accessible to everyone, resilient to network failures, and ranks better in search engines that may not execute JS.

Real-World Use

The UK Government Digital Service (GDS) mandates progressive enhancement for all government websites. Their MPAs work without JavaScript, then enhance with CSS and JavaScript when available. This ensures citizens can access essential services regardless of device, network, or disability.

Progressive Enhancement Layers
    ┌──────────────────────────────────────────────────────────┐
    │              Progressive Enhancement Layers               │
    ├──────────────────────────────────────────────────────────┤
    │                                                          │
    │                    / \                                   │
    │                   / JS \   Enhancement layer             │
    │                  /──────\  Interactions, animations,     │
    │                 /  CSS   \ AJAX, client-side validation  │
    │                /──────────\                              │
    │               /    HTML    \  Core layer                 │
    │              /──────────────\ Content, links, forms,    │
    │             /                \ navigation — works       │
    │            /                  \ without anything else   │
    │           /────────────────────\                         │
    │                                                          │
    │  Without JS: All content accessible, all forms work,    │
    │              all links navigate (full page loads)       │
    │                                                          │
    │  With JS: AJAX submissions, animations, inline           │
    │           validation, smooth transitions                │
    │                                                          │
    └──────────────────────────────────────────────────────────┘

Think of progressive enhancement like building a house. First you build the foundation and walls (HTML — people can live in it). Then you add electricity and plumbing (CSS — it looks nice). Finally you add smart home features (JS — it is convenient). The house is livable at every stage, and losing the smart features does not make it uninhabitable.

Core HTML Layer

<!-- This form works without ANY JavaScript -->
<form method="POST" action="/search"
      class="search-form"
      data-enhanced>
    <label for="search-input">Search products:</label>
    <input type="search"
           id="search-input"
           name="q"
           placeholder="Search..."
           required
           minlength="2">

    <button type="submit">Search</button>
</form>

<!-- Results display — works with both full page load and AJAX -->
<div id="search-results">
    <% if (locals.results) { %>
        <h2>Search Results for "<%= query %>"</h2>
        <% if (results.length > 0) { %>
            <ul>
                <% results.forEach(result => { %>
                    <li>
                        <a href="<%= result.url %>">
                            <%= result.title %>
                        </a>
                        <p><%= result.description %></p>
                    </li>
                <% }); %>
            </ul>
        <% } else { %>
            <p>No results found for "<%= query %>".</p>
        <% } %>
    <% } %>
</div>

Enhancement Layer with Feature Detection

// Progressive enhancement — add JS features only if supported
(function() {
    // Check if browser supports required features
    const supports = {
        fetch: 'fetch' in window,
        querySelector: 'querySelector' in document,
        formData: 'FormData' in window,
        history: 'pushState' in window.history
    };

    if (!supports.fetch || !supports.formData) {
        console.log('Browser does not support enhancement features');
        return; // Core HTML experience remains
    }

    // Enhance search form with AJAX
    const searchForm = document.querySelector('.search-form');
    if (!searchForm) return;

    // Remove required attribute — we handle validation ourselves
    // but the HTML validation still works as fallback
    searchForm.addEventListener('submit', async function(e) {
        e.preventDefault();

        const formData = new FormData(this);
        const query = formData.get('q');

        // Show loading state
        const resultsContainer = document.getElementById('search-results');
        resultsContainer.innerHTML = '<p aria-live="polite">Searching...</p>';

        try {
            const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
                headers: { 'Accept': 'text/html' }
            });

            if (!response.ok) {
                throw new Error('Search failed');
            }

            const html = await response.text();
            resultsContainer.innerHTML = html;

            // Update URL without page reload
            window.history.pushState(
                { search: query },
                '',
                `/search?q=${encodeURIComponent(query)}`
            );

            // Manage focus for screen readers
            resultsContainer.querySelector('h2')?.focus({ preventScroll: true });
        } catch (error) {
            // Fallback: full page load
            window.location.href = `/search?q=${encodeURIComponent(query)}`;
        }
    });
})();

Testing Progressive Enhancement

// Test that the application works without JavaScript
async function testProgressiveEnhancement() {
    const tests = [
        {
            name: 'Navigation works without JS',
            run: () => {
                // All links use normal <a href> — no onclick
                const links = document.querySelectorAll('a[onclick]');
                return links.length === 0;
            }
        },
        {
            name: 'Forms submit without JS',
            run: () => {
                const forms = document.querySelectorAll('form');
                return Array.from(forms).every(form => {
                    return form.action && form.method;
                });
            }
        },
        {
            name: 'Content is in HTML (not JS-rendered)',
            run: () => {
                const main = document.querySelector('main');
                return main && main.textContent.trim().length > 0;
            }
        },
        {
            name: 'No empty containers waiting for JS',
            run: () => {
                const containers = document.querySelectorAll('[data-js-content]');
                return Array.from(containers).every(container => {
                    return container.innerHTML.trim() !== '';
                });
            }
        },
        {
            name: 'Form labels are associated with inputs',
            run: () => {
                const inputs = document.querySelectorAll('input, select, textarea');
                return Array.from(inputs).every(input => {
                    return input.labels.length > 0 || input.getAttribute('aria-label');
                });
            }
        }
    ];

    const results = tests.map(test => ({
        name: test.name,
        passed: test.run()
    }));

    console.table(results);
    return results;
}

// Expected output:
// ┌────────────────────────────────────────────────────┬────────┐
// │ name                                              │ passed │
// ├────────────────────────────────────────────────────┼────────┤
// │ Navigation works without JS                       │ true   │
// │ Forms submit without JS                           │ true   │
// │ Content is in HTML (not JS-rendered)              │ true   │
// │ No empty containers waiting for JS                │ true   │
// │ Form labels are associated with inputs            │ true   │
// └────────────────────────────────────────────────────┴────────┘

Common Mistakes

  1. Empty containers that rely on JavaScript. Do not have empty divs that JavaScript populates. The core content should be in the HTML. JS should enhance, not create, content.
  2. onclick handlers on links. Links should have href attributes that work without JavaScript. Use onclick to enhance, not replace, default behavior.
  3. Client-side rendering of critical content. Content that users need (prices, descriptions, navigation) must be in the HTML. JavaScript should only enhance presentation.
  4. Not testing without JavaScript. Developers often test only with JavaScript enabled. Regularly test your application with JavaScript disabled to verify core functionality.
  5. Relying on JavaScript for form submission. Forms must work without JavaScript. The action and method attributes should point to a server endpoint that handles the submission.

Practice Questions

  1. What are the three layers of progressive enhancement?
  2. Why is it important for forms to work without JavaScript?
  3. How do you enhance a form with AJAX while keeping the fallback working?
  4. What is feature detection and how does it differ from browser detection?
  5. How do you test that an MPA works without JavaScript?

Challenge: Audit an existing MPA for progressive enhancement Compliance. Disable JavaScript in the browser and test: all navigation links work, all forms submit and show results, content is present on every page, no empty containers, error messages are displayed. Then enhance one form with AJAX while keeping the fallback working.

FAQ

Does progressive enhancement mean I cannot use JavaScript?

No. It means JavaScript is an enhancement, not a requirement. Core functionality works without JS. JavaScript adds convenience, speed, and polish.

How many users have JavaScript disabled?

Approximately 0.2 percent of users disable JavaScript, but many more (up to 5 percent) have JS unavailable due to network issues, corporate firewalls, or assistive technologies.

Is graceful degradation the same as progressive enhancement?

Graceful degradation builds for modern browsers first and tries to work on older ones. Progressive enhancement builds for the simplest browser first and adds layers.

Does progressive enhancement improve SEO?

Yes. Search engine crawlers can read HTML content without executing JavaScript. Progressively enhanced pages are indexed faster and more reliably.

How do I handle single-page app features with progressive enhancement?

Build the basic functionality with full page loads, then add client-side routing with JavaScript when available. The app works as an MPA without JS and as an SPA with JS.

Mini Project

Build a product catalog MPA that follows progressive enhancement: core HTML layer with all product data in the server-rendered page, CSS layer for Responsive Design and visual presentation, JS enhancement layer with AJAX search (fallback to full page load), client-side form validation (fallback to server validation), and smooth page transitions (Turbolinks as enhancement). Verify all functionality works with JavaScript disabled.

What's Next

You understand progressive enhancement. Now explore MPA Accessibility to build inclusive applications for all users.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro