Skip to main content
Saved
Foundation

The Progressive Enhancement Mindset

Build resilient web applications that work for everyone, even when failures occur.

Den Odell
By Den Odell Added
In this article
  1. TL;DR
  2. Defining Progressive Enhancement
  3. The Case for Progressive Enhancement
  4. The Layers of Enhancement
  5. Layer 1: HTML Foundation
  6. Layer 2: CSS Presentation
  7. Layer 3: JavaScript Enhancement
  8. Graceful Degradation
  9. Progressive Enhancement
  10. Misconceptions About Progressive Enhancement
  11. Myth: “PE means building everything three times”
  12. Myth: “PE means no JavaScript”
  13. Myth: “PE is only about users with JS disabled”
  14. Myth: “PE makes development slower”
  15. Myth: “Modern frameworks can’t do PE”
  16. Myth: “PE means ugly, basic websites”
  17. Thinking in Layers, Not Components
  18. The Reliability Spectrum
  19. HTML: Bulletproof
  20. CSS: Resilient
  21. JavaScript: Fragile
  22. Building for the Whole World
  23. Feature Detection Over Assumptions
  24. Defense in Depth
  25. PE in Practice
  26. Example 1: Form Submission
  27. Example 2: Navigation
  28. Modern Frameworks and Progressive Enhancement
  29. The Reality
  30. Modern Tools Enable This
  31. PE and Application Architecture
  32. Routing
  33. State Management
  34. Data Fetching
  35. Form Handling
  36. The Performance Philosophy
  37. The Testing Mindset
  38. Layer Testing
  39. Network Testing
  40. Embracing Uncertainty
  41. The Web’s Fault Tolerance
  42. Your Response Strategy
  43. The Path Forward
  44. Four Simple Steps
TL;DR

TL;DR

Progressive enhancement is a philosophy of building web apps from the ground up. Start with HTML that works everywhere, layer CSS for presentation, add JavaScript for enhancement.

Key principles:

Build in layers: HTML → CSS → JavaScript
Each layer is optional: no hard dependencies
Graceful failures: when one layer breaks, others keep working
Universal access: resilient and performant for everyone

To build something strong, begin with what's small.

— Lao Tzu

Defining Progressive Enhancement

Progressive enhancement is a way of approaching building for the web. You build the parts every user needs first, then add enhancements on top.

You start by building HTML that delivers content and core functionality to every user, regardless of their browser, device, or network conditions. You layer CSS on top to enhance visual presentation. Finally, you add JavaScript to provide richer interactive features in browsers that can run it.

Each layer enhances what came before, but nothing depends on the layers above working correctly.

It’s also a way of seeing the web: as a medium where browsers, devices, and networks vary widely, so you design for that whole range rather than only the ideal case.

If JavaScript fails to load, the CSS still makes your site presentable. Users may lose some functionality, but nothing fundamental. If CSS doesn’t load, the HTML still delivers all content. A failure at one layer never stops the layers below it from working.

This is different from building JavaScript-first applications, where nothing renders unless the scripts run successfully. Progressive enhancement treats capability as a spectrum, giving users the best experience their browser and network can support.

The Case for Progressive Enhancement

The web is unpredictable, and JavaScript fails to run more often than many developers realize:

  • Networks drop packets
  • CDNs experience outages
  • Corporate proxies block scripts
  • Browser extensions conflict
  • Ad blockers strip content
  • Mobile connections timeout

One uncaught error can break everything that follows.

When you build your site to require JavaScript just to display content, any of these failures means users see nothing.

On the web, failures are a normal part of how things run rather than rare exceptions, and progressive enhancement is one way of building for that.

The benefits:

  • Improved accessibility with semantic HTML
  • Better performance with less code
  • Easier maintenance through separation

It also changes the question you start from. Instead of asking “what features can I build?” you ask “what’s the core experience?”

The Layers of Enhancement

It’s like building a house: the foundation, frame, and walls go up before any smart home features, so if the automation breaks you still have a house to live in.

Layer 1: HTML Foundation

Structure, content, and basic functionality: forms submit, links navigate, and headings create hierarchy. This layer works in every browser.

Core capabilities:

  • Semantic structure
  • Content delivery
  • Form submission
  • Navigation

Layer 2: CSS Presentation

Typography, layout, color, and spacing, added without breaking any functionality. If CSS fails to load, the HTML still delivers the content.

Core capabilities:

  • Layout & grid
  • Typography
  • Responsive design
  • Visual hierarchy

Layer 3: JavaScript Enhancement

Interactivity and richer behavior, such as real-time updates, validation, and smooth transitions. The page still works without this layer.

Core capabilities:

  • Real-time updates
  • Client validation
  • Transitions
  • Rich interactions

You build one version, with each layer improving on the one before: the HTML works alone, CSS makes it look better, and JavaScript makes it easier to use.

Graceful Degradation vs Progressive Enhancement

Graceful Degradation

Building the full-featured version first, then adding fallbacks. You design for the best-case scenario and work backwards.

The mindset: “We will build this amazing experience, and if JavaScript is disabled, here is a fallback.”

Treats the baseline as an afterthought.

Progressive Enhancement

Building the baseline version first, then adding features for browsers that support them. You design for the worst-case scenario and work upwards.

The mindset: “We will make sure this works everywhere first, then make it amazing for capable browsers.”

Treats the baseline as the foundation.

When you start with the baseline, you make sure it works well, rather than leaving it as something that only just functions.

See Graceful Degradation for a deeper look at how this principle relates to progressive enhancement.

Misconceptions About Progressive Enhancement

A few common myths put teams off adopting PE.

Myth: “PE means building everything three times”

Reality: You build once. HTML provides structure, CSS styles it, and JavaScript enhances it, all in the same component.

Myth: “PE means no JavaScript”

Reality: PE means JavaScript is an enhancement rather than a requirement. You can use as much JS as you like, as long as core functionality doesn’t depend on it.

Myth: “PE is only about users with JS disabled”

Reality: It’s about resilience. Network failures, loading errors, script blockers, and performance issues can all stop JS from running, and any user can run into them.

Myth: “PE makes development slower”

Reality: PE often simplifies the architecture. Semantic HTML is easier to test and maintain. Clear separation of concerns reduces debugging time.

Myth: “Modern frameworks can’t do PE”

Reality: Modern frameworks like Next.js, Nuxt, React Router (formerly Remix), and SvelteKit support PE through server-side rendering.

Myth: “PE means ugly, basic websites”

Reality: Plenty of polished, well-designed websites use PE, including GitHub and GOV.UK.

A navigation menu is an HTML list of links, CSS positioning and styling, and JavaScript-enhanced interactions, and each of those layers works on its own.

Thinking in Layers, Not Components

Modern frontend development encourages thinking in components. A dropdown menu becomes a React component and a modal becomes a Vue component, and thinking that way can hide the fact that each of them is built from layers.

It helps to think about what each layer provides instead:

Your HTML should make sense by itself. That means semantic elements that convey meaning, a proper document structure, and meaningful content, without relying on JavaScript to generate the structure.

Your CSS should enhance without requiring JavaScript. The cascade, layout, and styling work with the HTML as it arrives, without assuming JavaScript will change the DOM before CSS applies.

Your JavaScript should add behavior that makes sense but isn’t required. You might intercept form submissions for a better user experience, but forms still work normally if JavaScript fails. Navigation can get smooth transitions, while the links keep working as standard links.

When layers fail independently, your site remains functional. No CSS means unstyled but usable content. No JavaScript means styled but un-enhanced interactions.

Learn more about implementing this in the practical guide Building with Progressive Enhancement.

The Reliability Spectrum

Each web technology has a different level of reliability in browsers. That difference explains why the layers go in this order.

HTML: Bulletproof

HTML has been part of the web since 1991, and over three decades browsers have become very forgiving of errors in it.

CSS: Resilient

Browsers handle CSS well, and failures are usually visual rather than functional. Browsers ignore properties they don’t support.

JavaScript: Fragile

JavaScript is the most fragile layer because scripts must download completely, and a single syntax error prevents the entire file from running.

This reliability spectrum maps directly to progressive enhancement:

  1. Build on the most reliable foundation (HTML)
  2. Enhance with the fairly reliable layer (CSS)
  3. Add the least reliable layer (JavaScript) last

JavaScript has the most ways to fail and the most severe consequences when it does. With JavaScript as the top layer, a script failure doesn’t break the core functionality underneath it.

Building for the Whole World

Progressive enhancement is also about reaching people whatever their device, network, or abilities.

In developing nations, mobile data is expensive and networks are slow. A 500KB JavaScript bundle can cost someone real money and take minutes to load. Starting with HTML means they can read your content as soon as it arrives.

For users with disabilities, semantic HTML provides the structure that assistive technologies rely on. Screen readers, switch controls, and voice navigation work best with properly structured HTML, not JavaScript-rendered div soup.

Progressive enhancement builds accessibility in from the start. When your baseline experience is semantic HTML, you’ve already met a good part of what WCAG asks for.

Feature Detection Over Assumptions

Progressive enhancement relies on detecting what a browser can do rather than assuming it.

When you assume every user has JavaScript enabled, or that every browser supports a particular API, you create a failure point. The reality is messier: some users disable JavaScript, some browsers lack support for newer APIs, and some network conditions prevent scripts from loading completely.

Feature detection means testing whether a browser supports a feature before using it. You check if the feature exists, not which browser or version someone uses. When a browser adds support later, your code starts using the feature without any changes.

Detection also needs a fallback for when a feature is missing, so that instead of breaking, the page does something simpler that provides the same core functionality.

See Feature Detection for implementation details.

Defense in Depth

Progressive enhancement follows the security principle of defense in depth: rather than relying on a single layer of protection, you build several, so that when one fails the others still work.

For web applications, this means critical functionality works at multiple levels. A form should work via traditional POST submission even if your fetch request fails. Navigation should work via standard HTML links even if your JavaScript router breaks. Content should be readable even if stylesheets fail to load.

Each layer is built to work on its own, without duplicating code across the layers.

Your HTML form works without JavaScript, and your JavaScript intercepts it to provide a better experience. When JavaScript fails, the form keeps working.

Critical paths are the user flows that must work for your site to serve its purpose. For an e-commerce site: browse products, add to cart, checkout, purchase. Each of those steps should work at the HTML level first, with enhancements added on top.

Learn more about the Defense in Depth principle, and see it applied in the Error Boundary Pattern.

PE in Practice

Two examples, a form and a navigation menu, show the three layers working together.

Example 1: Form Submission

The form starts as HTML that works without JavaScript, then gains client-side validation and optimistic UI.

Layer 1: HTML Foundation

<form action="/api/contact" method="POST">
  <label for="email">Email</label>
  <input type="email" id="email" name="email" required />
  <button type="submit">Send</button>
</form>

This works everywhere because the browser handles validation and submission natively.

Layer 2: CSS Enhancement

form {
  display: grid;
  gap: 1rem;
  max-width: 400px;
}

input:invalid {
  border-color: red;
}

input:valid {
  border-color: green;
}

button {
  padding: 0.75rem;
  background: blue;
  color: white;
}

This provides visual feedback without breaking the underlying HTML functionality.

Layer 3: JavaScript Enhancement

const form = document.querySelector('form');
const button = form.querySelector('button');

form.addEventListener('submit', async (e) => {
  e.preventDefault();

  const data = new FormData(e.target);

  // Show loading state
  button.disabled = true;

  const res = await fetch('/api/contact', {
    method: 'POST',
    body: data
  });

  // Handle response
});

This gives a better user experience with async submission and instant feedback, and if the script doesn’t load, the HTML form still works.

Example 2: Navigation

The navigation starts as standard links, then gains client-side routing for faster transitions.

Layer 1: HTML Foundation

<nav>
  <a href="/">Home</a>
  <a href="/about">About</a>
  <a href="/contact">Contact</a>
</nav>

These standard links work in all browsers and support keyboard navigation out of the box.

Layer 2: CSS Enhancement

nav {
  display: flex;
  gap: 2rem;
}

a {
  padding: 0.5rem 1rem;
  text-decoration: none;
  transition: all 0.2s;
}

a:hover {
  background: blue;
  color: white;
}

a[aria-current="page"] {
  font-weight: bold;
}

This adds visual polish and active state indicators while keeping the links fully functional.

Layer 3: JavaScript Enhancement

// Client-side routing
const nav = document.querySelector('nav');

nav.addEventListener('click', async (e) => {
  const link = e.target.closest('a');
  if (link) {
    e.preventDefault();

    const url = link.href;

    // Fetch & update
    const res = await fetch(url);
    const html = await res.text();

    // Update DOM (updatePage is your own function that swaps in the new content)
    updatePage(html);

    // Update history
    history.pushState({}, '', url);
  }
});

This changes pages without a full reload in browsers that run the script, and standard navigation still works if JavaScript fails.

The HTML works on its own, CSS adds polish, and JavaScript adds convenience. A failure in one layer never breaks the layers below it.

Modern Frameworks and Progressive Enhancement

Some developers assume modern JavaScript frameworks like React, Vue, or Svelte are incompatible with progressive enhancement, but you can build progressively enhanced applications with them.

Server-side rendering gives you the first layer of progressive enhancement: your framework renders HTML on the server and sends it to the browser, so the page is visible as soon as it arrives. JavaScript loads and “hydrates” the page, adding interactivity. If JavaScript fails, the server-rendered content still displays.

MPA vs SPA: A False Choice

The Reality

You don’t have to choose between multi-page applications (MPAs) and single-page applications (SPAs). Most real applications live somewhere in the middle.

That usually means server-rendered pages with JavaScript-enhanced interactions, where some sections behave like SPAs and others use traditional navigation. You can mix and match based on what each part of the app needs.

Modern Tools Enable This

Meta-frameworks like Next.js, Nuxt, React Router (formerly Remix), and SvelteKit support server-side rendering by default, making this approach easier.

The mindset counts for more than the tools: your server delivers meaningful content, and JavaScript enhances it rather than replacing it.

See Building with Progressive Enhancement for practical implementation with modern frameworks.

PE and Application Architecture

Progressive enhancement also changes how you structure an application.

Routing

Without PE: Client-side only routing where JavaScript handles all navigation, meaning nothing works if JS fails.

With PE: Server-rendered routes where every route returns full HTML, and the client-side router enhances with smooth transitions.

State Management

Without PE: Client-side state as the source of truth, with all app state living in JavaScript and lost on reload.

With PE: Server state as the foundation, with URL and server state being authoritative while client state enhances the user experience.

Data Fetching

Without PE: Fetch on mount, where the component mounts, shows a loader, and fetches data, leaving empty HTML until JS runs.

With PE: Server-side rendering where data is fetched server-side and rendered to HTML, then the client hydrates for interactivity.

Form Handling

Without PE: JavaScript-only forms that require JS handlers and disable native submission entirely.

With PE: HTML forms that work by submitting to server endpoints, enhanced with client-side validation and optimistic UI.

Next.js, Nuxt, React Router, SvelteKit, and Astro all support progressive enhancement, and they make it easier to build.

What modern frameworks provide:

  • Server-side rendering by default
  • Automatic code splitting and lazy loading
  • Form actions that work without JavaScript
  • Streaming and progressive hydration

The Performance Philosophy

Progressive enhancement also tends to produce faster applications.

When you build from a working baseline and add enhancements on top, you naturally defer non-critical code. The critical rendering path includes only HTML and critical CSS. JavaScript isn’t on the critical path because it’s not required for basic functionality.

As a result, first contentful paint happens sooner, and links and forms work before JavaScript has finished loading. The site also feels faster, because users see content while JavaScript is still downloading in the background.

Every framework and library you add increases bundle size, parse time, and execution cost. Progressive enhancement makes you justify each addition. A large framework is hard to justify for static content that HTML and CSS can display with a little selective enhancement.

The fastest code is the code you never load. Progressive enhancement helps you separate what's necessary from what's an enhancement.

The Testing Mindset

Progressive enhancement changes how you test, because you need to verify your site works at multiple levels rather than just in ideal conditions.

Testing Strategies

Layer Testing

Disable JavaScript in developer tools, then load your site and try to navigate, submit forms, and reach the critical content. If the page is blank or broken without JavaScript, it isn’t progressively enhanced.

Test across browsers and devices. Chrome features might not work in Safari or Firefox. Desktop layouts might break on mobile. Interactions built for a mouse might not work from the keyboard.

Network Testing

Throttle to 3G speeds, try loading with intermittent connectivity, and see what happens when scripts time out. These tests show how your site behaves on real-world connections.

The goal is resilience, not perfection at every layer. Core functionality should work regardless of circumstances. Enhanced experiences can fail without breaking everything.

Learn more in Building with Progressive Enhancement.

Embracing Uncertainty

Progressive enhancement is about accepting the web’s unpredictability. You can’t control which browser users have, their network quality, or whether firewalls and browser extensions interfere with your code.

You can control how your application responds. You can build systems that work in tough conditions, create experiences that adapt to what’s available, and write code that fails gracefully.

Ask 'what happens when this fails?' as well as 'will this work?', and build for realistic conditions rather than ideal ones.

That means treating failures as a normal part of running on the web, rather than as edge cases.

The Web’s Fault Tolerance

The web is fault-tolerant by design:

  • HTTP handles failures
  • Browsers ignore unsupported CSS
  • HTML parsers recover from errors

Your code can follow the same approach.

Your Response Strategy

When building with progressive enhancement:

  • When something fails, degrade gracefully
  • When a browser lacks support, provide a fallback
  • When the network is slow, show content progressively

The Path Forward

Progressive enhancement makes sites more accessible, faster, and more resilient, so they work for more people in more situations.

This approach takes practice. It requires thinking in layers rather than components, treating capability as a spectrum, and considering failure cases before success cases.

The result is an application that works reliably across browsers, devices, and network conditions, and adapts to its users instead of expecting them to adapt to it.

Start with HTML that delivers core functionality. Make sure it works. Add CSS to match your design. Test that styled HTML still works. Add JavaScript to enhance the experience.

Four Simple Steps

  1. Write HTML that works everywhere
  2. Add CSS to make it look right
  3. Add JavaScript to make it better
  4. Test at each layer

Key principle: Each layer enhances what came before, but nothing depends on layers above.

Related Content:

External Resources:

Newsletter

A Monthly Email
from Den Odell

Behind-the-scenes thinking on frontend patterns, site updates, and more

No spam. Unsubscribe anytime.