TL;DR
Progressive enhancement is a philosophy of building web apps from the ground up. You start with HTML that works everywhere, then layer CSS on top for presentation, and add JavaScript for enhancement.
Key principles:
To build something strong, begin with what's small.
Defining Progressive Enhancement
Progressive enhancement is a way of building for the web. You build the parts every user needs first, then add enhancements on top.
Everything starts with HTML that delivers content and core functionality to every user, regardless of their browser or network conditions. CSS goes on top to improve how it looks. Finally, JavaScript adds 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 thinking about the web. Browsers and networks vary a lot, 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. Your users might lose some functionality, but nothing fundamental. If CSS doesn’t load, the HTML still delivers all the 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. With progressive enhancement, each user gets 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 you might realize:
- Networks drop packets
- CDNs experience outages
- Corporate proxies block scripts
- Browser extensions conflict with scripts
- Ad blockers strip out content
- Mobile connections time out
One uncaught error can break everything that runs after it.
If your site needs JavaScript just to display content, any of these failures means your users see nothing.
Failures like these are a normal part of the web rather than rare exceptions, and progressive enhancement is one way of building for them.
The benefits:
- Improved accessibility with semantic HTML
- Better performance with less code
- Easier maintenance, because the layers are kept separate
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 and walls go up before any smart home features, so if the automation breaks, you still have a house to live in.
- HTML Foundation
Structure and basic functionality: forms submit and links navigate. This layer works in every browser.
Core capabilities
- Semantic structure
- Content delivery
- Form submission
- Navigation
- 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 and grid
- Typography
- Responsive design
- Visual hierarchy
- JavaScript Enhancement
Interactivity and richer behavior, such as real-time updates and validation. 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 without the other two, CSS makes it look better, and JavaScript makes it easier to use.
Graceful Degradation vs Progressive Enhancement
Graceful Degradation
You build the full-featured version first and then add fallbacks, so you’re designing for the best case and working backward.
The mindset: “We’ll build this amazing experience, and if JavaScript is disabled, here’s a fallback.”
This treats the baseline as an afterthought.
Progressive Enhancement
You build the baseline version first and then add features for browsers that support them, so you’re designing for the worst case and working upward.
The mindset: “We’ll make sure this works everywhere first, then make it amazing for capable browsers.”
This treats the baseline as the foundation.
When you start with the baseline, you make sure it works well, instead of leaving it as something that only just works.
Graceful Degradation takes a deeper look at how that principle relates to progressive enhancement.
Misconceptions About Progressive Enhancement
A few common myths put teams off adopting PE, and you’ve probably heard some of them.
Myth: “PE means building everything three times”
Reality: You only build it once, with HTML for structure, CSS for styling, and JavaScript for enhancement, 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, because network failures, loading errors, script blockers, and performance issues can all stop JS from running, and any of your users can run into them.
Myth: “PE makes development slower”
Reality: PE often makes your architecture simpler, because semantic HTML is easier to test and maintain, and when concerns are kept separate, you spend less time debugging.
Myth: “Modern frameworks can’t do PE”
Reality: Modern frameworks like Next.js, Nuxt, SvelteKit, and React Router, which used to be Remix, 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 keeps working if the ones above it fail.
Thinking in Layers, Not Components
Modern frontend development encourages you to think 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. You’d use semantic elements that convey meaning and a proper document structure, without relying on JavaScript to generate it.
Your CSS should enhance without requiring JavaScript. Your layout and styling should work with the HTML as it arrives, without assuming JavaScript will change the DOM before the 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 the layers fail independently, your site keeps working. Without CSS, your content is unstyled but usable, and without JavaScript, it's styled but missing the enhanced interactions.
The practical guide Building with Progressive Enhancement goes into how to implement this.
The Reliability Spectrum
Each web technology is reliable to a different degree in browsers, and that’s 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. They skip over any properties they don’t support.
JavaScript: Fragile
JavaScript is the most fragile layer, because a script has to download completely before it runs, and a single syntax error stops the whole file from running.
Progressive enhancement adds the layers in the same order as this spectrum:
- HTML, the most reliable, is the foundation
- CSS, which is fairly reliable, enhances it
- JavaScript, the least reliable, goes on last
JavaScript has the most ways to fail and the worst consequences when it does. With JavaScript as the top layer, a script failure in your app doesn’t break the core functionality underneath it.
Building for the Whole World
Progressive enhancement is also about making your site work for people whatever their 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. If you start with HTML, 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 and voice navigation work best with properly structured HTML, not JavaScript-rendered div soup.
With progressive enhancement, accessibility is part of your site 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’re building in something that can break. In practice, some browsers don’t support newer APIs, and scripts don’t always run, either because the user disabled JavaScript or because the network dropped them.
With feature detection, you test whether a browser supports a feature before you use it. You check whether the feature exists, not which browser or version someone uses, so when a browser adds support later, your code starts using the feature without any changes.
You also need a fallback for when a feature is missing, so that instead of breaking, the page does something simpler that still provides the same core functionality.
The Feature Detection pattern covers the 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.
In a web application, your critical functionality should work at more than one level. A form should still work through a traditional POST submission if your fetch request fails, and navigation should still work through standard HTML links if your JavaScript router breaks.
Each layer is built to keep working when the layers above it fail, without duplicating code across the layers.
Your HTML form works without JavaScript, and your JavaScript intercepts the submission to give users a better experience whenever the script runs.
Your critical paths are the user flows your site can’t do its job without. For an e-commerce site, that’s browsing products, adding to the cart, checking out, and paying. Each of those steps should work at the HTML level first, with enhancements added on top.
The Defense in Depth principle goes into this in more detail, and the Error Boundary Pattern shows it applied.
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;
}
The CSS adds visual feedback without breaking the HTML underneath it.
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 your users a better 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 provides a working page, CSS adds polish, and JavaScript adds convenience, so a failure in one layer never breaks the layers below it.
Modern Frameworks and Progressive Enhancement
You might 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. Then 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 are 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 your app needs.
Modern Tools Enable This
Meta-frameworks like Next.js, Nuxt, SvelteKit, and React Router, which used to be Remix, support server-side rendering by default, which makes this approach easier.
The mindset counts for more than the tools: your server delivers meaningful content, and JavaScript enhances it rather than replacing it.
Building with Progressive Enhancement shows how to implement this with modern frameworks.
PE and Application Architecture
Progressive enhancement also changes how you structure your application.
Routing
Without PE: Routing only happens on the client, with JavaScript handling all navigation, so nothing works if JS fails.
With PE: Each route is rendered on the server and returns full HTML, and the client-side router adds smooth transitions on top.
State Management
Without PE: Client-side state is the source of truth, so all your app state is kept in JavaScript and lost on reload.
With PE: Server state is the foundation. The URL and server state are the source of truth, and client state improves the user experience on top.
Data Fetching
Without PE: With fetch on mount, the component mounts, shows a loader, and then fetches data, so the HTML is empty until JS runs.
With PE: With server-side rendering, data is fetched on the server and rendered to HTML, then the client hydrates the page for interactivity.
Form Handling
Without PE: Forms only work through JavaScript, because they need JS handlers and disable native submission entirely.
With PE: HTML forms work by submitting to server endpoints, and client-side validation and optimistic UI enhance them.
Next.js, Nuxt, React Router, SvelteKit, and Astro all support progressive enhancement, and they make progressively enhanced apps 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 end up deferring code that isn’t critical. Your critical rendering path only includes HTML and critical CSS, and JavaScript isn’t on it, because your basic functionality doesn’t need JavaScript.
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.
Each framework and library you add increases your bundle size and the time the browser spends running scripts. With progressive enhancement, you have to justify each one you add. 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 check that your site works at more than one level, rather than just in ideal conditions.
Testing Strategies
Layer Testing
With JavaScript disabled in your browser’s developer tools, you can load your site and try navigating and submitting forms. If the page is blank or broken, it isn’t progressively enhanced.
Testing across browsers and devices helps too, because Chrome features might not work in Safari or Firefox, and interactions built for a mouse might not work from a keyboard or touchscreen.
Network Testing
Throttling to 3G speeds shows what happens when your scripts time out, and how your site behaves on real-world connections.
The goal is resilience rather than perfection at every layer. Your core functionality should work whatever the conditions, and the enhanced experience can fail without breaking everything.
There’s more on this in Building with Progressive Enhancement.
Embracing Uncertainty
Progressive enhancement is about accepting that the web is unpredictable. You can’t control your users’ network quality, or whether browser extensions interfere with your code.
You can control how your application responds, though, by building it to adapt to what’s available and fail gracefully when something is missing.
If you ask 'what happens when this fails?' as well as 'will this work?', you end up building for realistic conditions rather than ideal ones.
You treat 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
With progressive enhancement, your code should respond like this:
- When something fails, it degrades gracefully
- When a browser lacks support, there’s a fallback
- When the network is slow, content appears progressively
The Path Forward
Progressive enhancement makes sites more resilient, so they work for more people in more situations. Accessibility and speed tend to improve along the way.
This approach takes practice, because you have to think in layers rather than components, and think about how things fail before how they succeed.
You end up with an application that works reliably across browsers and network conditions, and adapts to your users instead of expecting them to adapt to it.
You start with HTML that delivers core functionality and make sure it works. Then you add CSS to match your design, check that the styled HTML still works, and add JavaScript to enhance the experience.
Four Simple Steps
- HTML that works everywhere comes first
- CSS goes on top to make it look right
- JavaScript goes on top of that to make it better
- Each layer gets tested as it’s added
Key principle: Each layer enhances what came before, but nothing depends on the layers above it.
Related Content:
- Foundations: Accessibility as a Foundation, Performance as a Foundation, Separation of Concerns
- Principles: Graceful Degradation, Defense in Depth, Baseline-First Thinking
- Guides: Building with Progressive Enhancement
- Patterns: Feature Detection, Form Enhancement Pattern, Navigation Enhancement Pattern, Content Loading Pattern, State Management Pattern
External Resources:
- Understanding Progressive Enhancement - A List Apart’s foundational article
- Progressive Enhancement: What It Is, And How To Use It? - Smashing Magazine guide
- The Power of Progressive Enhancement - Modern take on progressive enhancement
- Resilient Web Design - Book by Jeremy Keith on building for the web
- Gov.UK Design System: Progressive Enhancement - Government Digital Service approach
- MDN: Feature Detection - Detecting browser capabilities