Saved
Expert Guide

Building with Progressive Enhancement

Guide to building resilient web applications that work for everyone, regardless of browser capabilities.

Icon for Building with Progressive Enhancement
Level
Beginner
Type
Framework

By Den Odell

Framework Guide

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 the History 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 pushState and handle popstate events. 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-motion and 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.

Stay Updated

Get New Patterns
in Your Inbox

Join thousands of developers receiving regular insights on frontend architecture patterns

No spam. Unsubscribe anytime.