Building with Progressive Enhancement
Your fancy JavaScript framework? It’s a bonus feature, not a requirement. Progressive enhancement means starting with HTML that actually works, then layering on the bells and whistles. It’s not about supporting ancient browsers; it’s about building apps that don’t collapse like a house of cards when one thing goes wrong.
Start with Working HTML
Here’s a radical idea: make your forms actually submit. I know, shocking. The baseline experience should work without JavaScript because JavaScript will fail. It’s not if, it’s when:
- Forms submit via POST: Use standard form actions. When your CDN goes down at 2am and JavaScript stops loading, users can still checkout. Revolutionary.
- Links navigate normally: Regular anchor tags work on every browser ever made. Use Navigation Enhancement to upgrade them with client-side routing later, but don’t make navigation depend on it.
- Content renders server-side: Deliver real HTML with Content Loading, not a blank page with a loading spinner while 500KB of JavaScript downloads over a spotty connection.
This baseline saves you when things go sideways, and they will. Corporate firewalls block your scripts, CDNs fail, users browse on ancient devices, ad blockers get aggressive. A working HTML foundation means your app degrades gracefully instead of showing a white screen of death.
Detect Features Before Using Them
Assuming every browser supports your favorite API is how you ship bugs to production. Check first, enhance second:
- Test API availability: Use Feature Detection before touching
IntersectionObserver,ResizeObserver, or theHistory API. If it doesn’t exist, your fallback runs. Simple. - Provide sensible fallbacks: No intersection observers? Load images immediately. User prefers reduced motion? Skip your fancy parallax scrolling. The app still works, just less flashy.
- Never use user agent sniffing: Parsing user agent strings to guess browser capabilities is like reading tea leaves to predict the weather. It’s unreliable, breaks constantly, and tells you nothing about what the browser can actually do. Test for the feature itself.
Layer Enhancements Progressively
Think of your app like a cake: each layer builds on the last, and if the frosting falls off, you’ve still got cake. Here’s how it stacks:
Layer 1: Core HTML
Server-rendered content with working forms and navigation.
<!-- Works without any JavaScript -->
<form method="POST" action="/search">
<input type="search" name="query" required>
<button type="submit">Search</button>
</form>
Layer 2: Basic Interactivity
Enhance forms and navigation when JavaScript loads.
// Enhance form with client-side submission
const form = document.querySelector('form');
if (form && 'fetch' in window) {
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
// Fetch results without full page reload
const response = await fetch('/search', {
method: 'POST',
body: formData
});
updateResults(await response.json());
});
}
Layer 3: Rich Features
Add advanced features only when supported.
// Add real-time search suggestions if supported
if ('IntersectionObserver' in window) {
const input = document.querySelector('input[type="search"]');
input.addEventListener('input', debounce(async () => {
const suggestions = await fetch(`/suggest?q=${input.value}`);
showSuggestions(await suggestions.json());
}, 300));
}
Handle Navigation Gracefully
Single-page apps love to break navigation. Don’t be that app:
- Links work by default: Start with real
<a>tags. If JavaScript never loads, navigation still works. Groundbreaking. - Intercept strategically: Use Navigation Enhancement to upgrade internal links with client-side routing, but respect modifier keys (Cmd+click) and external links. Let browsers do what they do best.
- Manage history properly: Update the URL with
pushStateand handlepopstateevents. The back button has worked since 1993; don’t break it now. - Handle failures: If client-side navigation bombs, catch it and fall back to a full page reload. A slow navigation beats a broken navigation every time.
Design for Varied Capabilities
Not everyone browses like you do. Some users navigate by keyboard, some find animations nauseating, some rely on screen readers:
- Respect reduced motion: Check
prefers-reduced-motionand dial down your animations. That spinning loader might look cool to you, but it makes some users physically ill. - Support keyboard navigation: Make every interactive element work with Tab, Enter, and arrow keys. Use Focus Management so keyboard users don’t get lost in your UI.
- Announce dynamic changes: When content updates, tell screen reader users about it with ARIA live regions. They can’t see your slick transition animations.
Test the Baseline
Building progressive enhancement without testing it is like writing tests that never run: pointless. Actually verify the fallbacks work:
- Disable JavaScript: Turn it off in your browser settings and try to use your app. If critical features break completely, you’re not progressively enhancing, you’re just building a JavaScript-only app with extra steps.
- Throttle network: Slow 3G isn’t a theoretical concern, it’s Tuesday afternoon for millions of users. Does your app still load content in a reasonable time, or does it stall indefinitely?
- Test older browsers: Fire up a browser without your favorite modern APIs. Do your feature detections actually work, or did you just ship “undefined is not a function” to production?
Balance Enhancement with Simplicity
Let’s be clear: progressive enhancement doesn’t mean supporting Netscape Navigator. Be pragmatic:
- Choose your baseline thoughtfully: You don’t need to support IE6. But your HTML should work without JavaScript on browsers people actually use today. That’s a reasonable bar.
- Document your assumptions: Write down your baseline requirements. Maybe you need CSS Grid but not JavaScript. Maybe your admin dashboard requires JS but your marketing pages don’t. Make it explicit.
- Enhance strategically: Not everything needs to work without JavaScript. A real-time collaborative editor? Yeah, that needs JS. A blog post? That should render without it. Use your judgment.
Make Enhancement Routine
Progressive enhancement works best when it’s not a special effort; it’s just how you build:
- Start with HTML: Write the markup and server routes first. Get them working, then sprinkle JavaScript on top. This forces you to think about the baseline instead of bolting it on later.
- Default to standard elements: Use native
<button>,<input>, and<a>tags before reaching for custom components. They work everywhere, for free. Your custom dropdown that required three files and a prayer? Native<select>already works. - Test as you build: Verify each enhancement degrades gracefully before adding the next one. Catching issues early beats debugging a broken enhancement chain later.
Remember: Users don’t care that you used the latest framework or built a clever abstraction. They care that the checkout button works when they click it. Progressive enhancement means you ship apps that work, not apps that impress other developers.