Skip to the content.

Progressive enhancement still matters, even in a JavaScript world

Progressive enhancement still matters, even in a JavaScript world

Progressive enhancement is the idea that you build on a baseline that works without JavaScript — real HTML, real forms, real links — and then layer richness on top for browsers that can take it. In an era where everything is a JavaScript app, it sounds quaint. It stops sounding quaint the first time a script fails to load on a flaky network, a CDN has a bad day, a browser extension breaks your bundle, or a crawler that does not run JS visits your page. In every one of those cases, the app that had a working HTML baseline degrades gracefully, and the app that assumed JavaScript shows a blank screen. Enhancement is insurance you only notice when the JavaScript isn’t there.

A working HTML base with JS enhancement on top degrades; a JS-only app collapses Left: layers — HTML baseline works, CSS styles, JS enhances; if JS fails the base still works. Right: a JS-only app that shows nothing when the script fails. progressive enhancement JS: richer interactions CSS: presentation HTML: works alone JS fails → base still usable JS-only JS fails → blank
With a real HTML base, losing JavaScript costs you the enhancements but keeps the app usable. Without one, losing JavaScript costs you everything.

Start with HTML that actually works

The baseline is not a fallback you add later — it is what you build first. A form that submits to a server endpoint works with zero JavaScript; a link that points to a real URL navigates without a router. Build those, then enhance:

<!-- works with no JS: real action, real method, real inputs -->
<form action="/search" method="GET">
  <label for="q">Search</label>
  <input id="q" name="q" type="search">
  <button type="submit">Search</button>
</form>

Submit that with JavaScript disabled and it still searches, because the browser knows how to submit a form. The baseline is free — it is just using the platform as intended.

Enhance without breaking the base

Then JavaScript intercepts and improves — debounced live results, no full reload — while leaving the working form underneath. The enhancement hijacks the submit only when it can, so if the script never runs, the native submit still fires:

// enhance the working form; if this never runs, the plain form still submits
form.addEventListener("submit", (e) => {
  if (!window.fetch) return;              // no capability? let the native submit happen
  e.preventDefault();                      // capable: take over for a nicer experience
  liveSearch(new FormData(form).get("q"));
});

The key discipline: JavaScript adds to a working thing, it does not replace a broken one. That is the difference between graceful degradation and a blank page.

The pragmatic middle: SSR is enhancement’s modern form

You do not have to make a rich SPA fully work without JavaScript to get most of the benefit — the modern, pragmatic version of progressive enhancement is server rendering. Ship real HTML content for the first paint (so crawlers, slow devices, and a failed hydration all get something meaningful), then hydrate to the rich app. For core flows that must be robust — sign-in, checkout, search — invest in a genuine no-JS-capable baseline; for the deeply interactive parts, SSR-plus-hydration is the sensible compromise. Either way the principle holds: never let a single failed script be the difference between a working page and a blank one. The form-field-molecule and responsive-image-set exercises both build on platform primitives (the form element, the img element with srcset) that already work without JavaScript — the baseline you enhance from rather than replace.