Progressive form enhancement: building resilient forms

Home / Everything About / Everything About Forms / Progressive form enhancement: building resilient forms

A visitor's internet cuts out while they are loading your form. A Chrome extension breaks your JavaScript. An older browser does not support the JavaScript you are using. Their screen reader cannot navigate your form because the labels are hidden with CSS.

Forms are critical paths. Users need to submit them. The form cannot just break. Progressive enhancement is the principle of building forms that work everywhere, then adding improvements for browsers that can handle them.

This article covers how to build forms that degrade gracefully, work without JavaScript, and remain accessible to everyone.

What is progressive enhancement?

Progressive enhancement means starting with a basic, working form that works in the oldest browsers and without JavaScript, then layering improvements on top for modern browsers. Layer 1 (HTML) provides a basic form with input fields, labels, and a submit button that needs no JavaScript and works everywhere. Layer 2 (CSS) styles the form with colors, spacing, and layout, so the form still works if CSS fails to load, though it just looks bad. Layer 3 (JavaScript) adds real-time validation, smooth animations, and AJAX submission, so the form still works if JavaScript fails and just submits traditionally. If any layer fails, the user still gets a working form. This is resilience.

The baseline: HTML that works

Start with semantic HTML using proper labels, fieldsets, and legend elements. A baseline form looks like <form action="/submit" method="POST"> <label for="email">Email</label> <input type="email" id="email" name="email" required> <label for="message">Message</label> <textarea id="message" name="message" required></textarea> <button type="submit">Send</button> </form>. This form works without JavaScript, without CSS, in old browsers, with screen readers, and with keyboard navigation. This is your baseline. Never remove elements from the DOM and replace them with JavaScript-rendered elements because if the JavaScript fails, the form breaks. Keep the semantic HTML.

Graceful degradation with CSS

Layer CSS enhancements on top of the HTML. Style the form nicely. But if CSS fails, the form still works.

Do not hide labels with CSS and rely on placeholders because some CSS fails to load and screen readers need labels in the HTML. Do not use CSS to hide required indicators because if your custom "required star" does not load, users cannot tell which fields are required. Use the HTML required attribute instead. Avoid hiding the label visually and showing a placeholder instead using label { display: none; } and input::placeholder { content: 'Email'; }. Instead, keep the label in the HTML and style it however you want using label { font-weight: bold; color: #333; }.

JavaScript enhancements that degrade

Add JavaScript improvements, but only if the form already works. Use feature detection to check if the browser supports what you need using if (window.fetch) { // Use fetch for AJAX submission } else { // Form submits traditionally }. Do not assume features exist. Assume they might not. Detect first.

Real-time validation with fallback

Use HTML validation as the baseline using <input type="email" required> so the browser validates on submit. Then add JavaScript validation that runs in real-time using input.addEventListener('blur', () => { if (!isValid(input.value)) { input.classList.add('invalid'); showError(input, 'Invalid email'); } });. It shows errors as the user types, and if JavaScript fails, the browser's built-in validation still works. The user gets the benefit of real-time feedback, and if JavaScript fails, they get submit-time feedback from the browser. Either way, validation happens.

AJAX submission with traditional fallback

Build a traditional form that submits server-side, then intercept the submit and use AJAX instead using form.addEventListener('submit', async (e) => { e.preventDefault(); const response = await fetch('/submit', { method: 'POST', body: new FormData(form) }); if (response.ok) { showMessage('Success'); } });. If JavaScript fails, the preventDefault never runs, the form submits the traditional way, and the user gets a page reload instead of smooth UX. But they get the form to work.

Accessibility as progressive enhancement

Accessibility is not an extra feature. It is the baseline. Build accessible forms.

Labels must be paired with inputs using for and id attributes. Not data-attributes, not aria-labels that can be missed. Proper labels.

<label for="email">Email</label>

<input id="email">

Use semantic HTML elements (fieldset, legend, button, input). Not divs styled to look like buttons.

Keyboard navigation must work. Tab through all fields. Shift+Tab to go back. Enter to submit. Space to toggle checkboxes. This works by default with semantic HTML. Do not break it with JavaScript.

After the basics work, add ARIA labels for screen readers. Add focus indicators for keyboard users. But these are enhancements on top of accessible HTML.

Testing for resilience

Test your form with JavaScript disabled. Disable JavaScript in your browser and try to submit. If the form breaks, it is not resilient.

Test with slow networks. Throttle your connection to 2G. If the form takes forever to load, visitors with spotty internet will leave.

Test with older browsers. Use BrowserStack or similar to test in IE 11 (if you still need to). See what features are not supported.

Test with screen readers. Use NVDA (Windows) or VoiceOver (Mac). Listen to how the form is announced. If it makes no sense to a screen reader, fix it.

Test with keyboard only. Do not use a mouse. Can you navigate the form? Can you submit it?

Feature detection, not browser detection

Do not check for specific browsers. Check for features instead of using browser detection like if (navigator.userAgent.includes('Chrome')) { /* use fetch */ }. Instead, use feature detection like if (window.fetch) { /* use fetch */ }. Feature detection is more reliable because a browser update that adds fetch support will work with your code, but browser detection will not.

How WEMASY builds resilient forms

WEMASY forms are built on progressive enhancement principles where they work without JavaScript, have labels and semantic HTML in place, have optional JavaScript enhancements, so older browsers get a working form and modern browsers get smooth interactions.

See resilience features in your WEMASY plan.

Frequently asked questions

Does my form really need to work without JavaScript?

How do I test if my form works without JavaScript?

What is feature detection and why is it better than browser detection?

Should I hide form labels and rely on placeholders?

How do I make AJAX submission work with traditional fallback?

What should I test to ensure my form is resilient?