Progressive enhancement as policy
A belief you can't fail isn't a policy. Write it as claims somebody could check.
There’s a line in one of our proposal templates that says we build with progressive enhancement. It’s been there for years. It sits between a line about accessibility and a line about performance, and all three are the same sort of sentence.
A client asked what it meant. Not adversarially, they were interested. And what came out of the four of us was about 90 seconds of gesturing: it works without JavaScript, sort of, and it degrades gracefully, and we build from the content out.
None of which is wrong. None of it is checkable either. They could not have taken any of those sentences away and used them to determine whether we’d done what we said, and neither could we.
A belief you can’t fail
That’s the problem with the way this gets talked about, and it’s why the phrase has had such a strange decade. It’s held as a philosophy, and a philosophy can’t be falsified, so it survives contact with reality by quietly becoming a preference.
Which means when the deadline gets tight, progressive enhancement isn’t a requirement being dropped, it’s a value being deprioritized, and nobody has to log that anywhere.
The version I’ve been trying since spring is to write it the same way I write a browser support matrix: as a small number of statements about what works under what conditions, each of which is either true or false about a given build.
Held as a philosophy it can't be falsified, so it survives by becoming a preference, and preferences are what get dropped in week eleven without anybody logging it.
Write it as claims
Four sentences, and they go in the proposal in place of the bullet point:
Every page’s primary content is present in the HTML the server returns. Not assembled afterward.
Every form can be completed and submitted without JavaScript, and gives the person a sensible response when it is.
Every navigation is a link with a real href that works if you open it in a new tab, copy it, or bookmark it.
Enhancements are layered on top, and their absence leaves something usable rather than something broken.
Each of those is testable in about a minute. Turn JavaScript off in devtools and walk the three journeys that make the client money. If the content is there and the form submits and the links go places, it’s true. If it isn’t, it isn’t, and that’s a bug with a ticket rather than a value we’ve deprioritized.
And it’s assertable in CI, which was the thing that made it real for us. A build check that fetches the deployed page and asserts that the main heading and the product name appear in the raw HTML response takes four lines and catches the regression that would otherwise arrive quietly with someone’s refactor.
It doesn’t fail because people turn it off
The usual objection is that nobody disables JavaScript, and that’s true, and it’s answering a question nobody’s asking.
The realistic failure cases are, in rough order of frequency: the bundle didn’t finish downloading, because a train went into a tunnel. A corporate proxy or security appliance mangled it. A blocker took out one third-party script and the error cascaded. The CDN had four bad minutes. An older browser hit a syntax it couldn’t parse and stopped executing the whole file. Or, most common of all, one of those third-party scripts I keep going on about threw an exception early and everything after it in the queue never ran.
None of those is a person with an opinion about JavaScript. They’re all just Tuesday.
The published figures I’ve seen for how often the JavaScript doesn’t run on a large site land somewhere around 1 in 100 sessions. Which sounds like nothing until you multiply it by a client’s traffic and discover it’s several thousand people a month, arriving at a page that looks like a spinner or a blank div, and not one of them will contact you about it.
Measure it, which nobody does
The single most useful thing in this post, and it takes an afternoon.
Put a counter in the HTML that the server increments when it renders the page. Put a second counter that only fires from JavaScript after your bundle has executed. Compare them over two weeks.
<!-- rendered server-side, always present -->
<img src="/beacon?stage=html" width="1" height="1" alt="">
<!-- fires only if the bundle parsed and ran -->
<script>window.addEventListener('load', () => {
navigator.sendBeacon('/beacon?stage=js');
});</script>
The ratio between those two numbers is your actual JavaScript failure rate, on your actual site, for your actual audience. It’s not a figure from an article, it’s not an argument, and it’s not my opinion versus the client’s marketing agency.
Every time I’ve done it the number has been higher than anyone in the room guessed, including me. And the conversation afterward is completely different, because you’re no longer discussing a philosophy, you’re discussing what should happen to the 4,000 people a month for whom the enhancement didn’t arrive.
The honest exceptions
I’d lose credibility fast if I pretended this scales to everything.
Some things genuinely cannot be done without JavaScript. A rich text editor. A drag-and-drop page builder. A live dashboard. An interactive map. Insisting on a no-JavaScript version of those isn’t principle, it’s obstruction, and I’ve watched people argue for it and lose the room deservedly.
What the policy asks in those cases isn’t a full alternative, it’s that the failure is graceful and there’s a route. A map degrades to an address, a zip code, and a link that opens a mapping service. A drag-and-drop reorder degrades to a number field and a save button, which takes an hour and covers the keyboard case as well. A dashboard degrades to a page that says what’s wrong and offers a refresh, rather than an empty gray rectangle.
And I’d concede the other half too: “the base layer is free if you start with it” is a thing developers say, and it’s mostly true and not always. On genuinely application-shaped products the base layer is real work, and it should be estimated as real work rather than smuggled in as a principle.
Four sentences in the proposal
The reason to write it down at the start rather than the end is that it is one of the very few things in this job that cannot be retrofitted at a sensible price. Adding a server-rendered base layer to a finished client-side app is a rebuild. Starting with one and layering on top costs a bit of discipline and almost no money.
So it belongs in the proposal, next to the browser matrix, as four sentences somebody could check.
I’d also say, having looked: there’s remarkably little written about this as a practice rather than as a position. A great many people are in favor of it and very few have published what they actually promise. Which might be why the client’s question was so hard to answer.