Progressive Enhancement in MPAs — Building Without JavaScript Dependency
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
- 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.
- onclick handlers on links. Links should have href attributes that work without JavaScript. Use onclick to enhance, not replace, default behavior.
- Client-side rendering of critical content. Content that users need (prices, descriptions, navigation) must be in the HTML. JavaScript should only enhance presentation.
- Not testing without JavaScript. Developers often test only with JavaScript enabled. Regularly test your application with JavaScript disabled to verify core functionality.
- 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
- What are the three layers of progressive enhancement?
- Why is it important for forms to work without JavaScript?
- How do you enhance a form with AJAX while keeping the fallback working?
- What is feature detection and how does it differ from browser detection?
- 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
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