Bringing Back
Progressive Enhancement
Kent C. Dodds




Let's wake up

Your brain needs this ðŸ§
What this talk is
- An explanation of Progressive Enhancement
- A demonstrations of what it can do for us
- A mic-drop moment 🎤

What this talk is not
- JS hate-town

Let's
Get
STARTED!


Why was web dev so much easier before?
What changed?
Progressive Enhancement
if (navigator.geolocation) {
// use geolocation API
}
Progressive Enhancement
-
Basic content should be accessible to all web browsers.
-
Basic functionality should be accessible to all web browsers.
-
Sparse, semantic markup contains all content.
-
Enhanced layout is provided by externally linked CSS.
-
Enhanced behavior is provided by externally linked JavaScript.
-
End-user web browser preferences are respected.
Enabling
Enhancing
<
without JS???





- Built with Remix
- Backend persistence
- User Auth
- Progressively enhanced
- 15% client-side JS
Built-in Mutations

<form method="post" action="/todos" enctype="application/x-www-form-urlencoded">
<input type="hidden" name="todoId" value="cl7yrv54a0045j1dz7etu1u9z" />
<input type="hidden" name="complete" value="true" />
<button
type="submit"
name="intent"
value="toggleTodo"
class="toggle"
title="Mark as complete"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="40"
height="40"
viewBox="-3 -3 105 105"
>
<circle
cx="50"
cy="50"
r="50"
fill="none"
stroke="#ededed"
stroke-width="3"
></circle>
</svg>
</button>
</form>

Why was web dev so much easier before?
What changed?
The Mental Model
(state management)
MPA mental model
SPA mental model
>
MPA capabilities
SPA capabilities
<
MPA mental model
SPA capabilities
+

Dev Experience
User Experience
There is no application state management with Remix.
Even if you are building Figma!

There is no application state management with Remix.
Even if you are building Figma!

Pending UI
function ListItem({ todo, filter }: { todo: TodoItem; filter: Filter }) {
const updateFetcher = useFetcher();
const toggleFetcher = useFetcher();
const deleteFetcher = useFetcher();
const updateFormRef = React.useRef<HTMLFormElement>(null);
const complete = todo.complete;
const shouldRender =
filter === "all" ||
(filter === "complete" && complete) ||
(filter === "active" && !complete);
if (!shouldRender) return null;
return (
<li className={complete ? "completed" : ""}>
<div className="view">
<toggleFetcher.Form method="post">
<input type="hidden" name="todoId" value={todo.id} />
<input type="hidden" name="complete" value={(!complete).toString()} />
<button
type="submit"
name="intent"
value="toggleTodo"
className="toggle"
title={complete ? "Mark as incomplete" : "Mark as complete"}
>
{complete ? <CompleteIcon /> : <IncompleteIcon />}
</button>
</toggleFetcher.Form>
<updateFetcher.Form
method="post"
className="update-form"
ref={updateFormRef}
>
<input type="hidden" name="intent" value="updateTodo" />
<input type="hidden" name="todoId" value={todo.id} />
<input
name="title"
className="edit-input"
defaultValue={todo.title}
onBlur={(e) => {
if (todo.title !== e.currentTarget.value) {
updateFetcher.submit(e.currentTarget.form);
}
}}
aria-invalid={updateFetcher.data?.error ? true : undefined}
aria-describedby={`todo-update-error-${todo.id}`}
/>
{updateFetcher.data?.error && updateFetcher.state !== "submitting" ? (
<div
className="error todo-update-error"
id={`todo-update-error-${todo.id}`}
>
{updateFetcher.data?.error}
</div>
) : null}
</updateFetcher.Form>
<deleteFetcher.Form method="post">
<input type="hidden" name="todoId" value={todo.id} />
<button
className="destroy"
title="Delete todo"
type="submit"
name="intent"
value="deleteTodo"
/>
</deleteFetcher.Form>
</div>
</li>
);
}
function ListItem({ todo, filter }: { todo: TodoItem; filter: Filter }) {
const updateFetcher = useFetcher();
const toggleFetcher = useFetcher();
const deleteFetcher = useFetcher();
const updateFormRef = React.useRef<HTMLFormElement>(null);
const isToggling = Boolean(toggleFetcher.submission);
const complete = todo.complete;
const shouldRender =
filter === "all" ||
(filter === "complete" && complete) ||
(filter === "active" && !complete);
if (!shouldRender) return null;
return (
<li className={complete ? "completed" : ""}>
<div className="view">
<toggleFetcher.Form method="post">
<input type="hidden" name="todoId" value={todo.id} />
<input type="hidden" name="complete" value={(!complete).toString()} />
<button
type="submit"
name="intent"
value="toggleTodo"
className="toggle"
title={complete ? "Mark as incomplete" : "Mark as complete"}
disabled={isToggling}
>
{complete ? <CompleteIcon /> : <IncompleteIcon />}
</button>
</toggleFetcher.Form>
<updateFetcher.Form
method="post"
className="update-form"
ref={updateFormRef}
>
<input type="hidden" name="intent" value="updateTodo" />
<input type="hidden" name="todoId" value={todo.id} />
<input
name="title"
className="edit-input"
defaultValue={todo.title}
onBlur={(e) => {
if (todo.title !== e.currentTarget.value) {
updateFetcher.submit(e.currentTarget.form);
}
}}
aria-invalid={updateFetcher.data?.error ? true : undefined}
aria-describedby={`todo-update-error-${todo.id}`}
disabled={isToggling}
/>
{updateFetcher.data?.error && updateFetcher.state !== "submitting" ? (
<div
className="error todo-update-error"
id={`todo-update-error-${todo.id}`}
>
{updateFetcher.data?.error}
</div>
) : null}
</updateFetcher.Form>
<deleteFetcher.Form method="post">
<input type="hidden" name="todoId" value={todo.id} />
<button
className="destroy"
title="Delete todo"
type="submit"
name="intent"
value="deleteTodo"
disabled={isToggling}
/>
</deleteFetcher.Form>
</div>
</li>
);
}

SPA capabilities + MPA mental model
Optimistic UI
function ListItem({ todo, filter }: { todo: TodoItem; filter: Filter }) {
const updateFetcher = useFetcher();
const toggleFetcher = useFetcher();
const deleteFetcher = useFetcher();
const updateFormRef = React.useRef<HTMLFormElement>(null);
const isToggling = Boolean(toggleFetcher.submission);
const complete = todo.complete;
const shouldRender =
filter === "all" ||
(filter === "complete" && complete) ||
(filter === "active" && !complete);
if (!shouldRender) return null;
return (
<li className={complete ? "completed" : ""}>
<div className="view">
<toggleFetcher.Form method="post">
<input type="hidden" name="todoId" value={todo.id} />
<input type="hidden" name="complete" value={(!complete).toString()} />
<button
type="submit"
name="intent"
value="toggleTodo"
className="toggle"
title={complete ? "Mark as incomplete" : "Mark as complete"}
disabled={isToggling}
>
{complete ? <CompleteIcon /> : <IncompleteIcon />}
</button>
</toggleFetcher.Form>
<updateFetcher.Form
method="post"
className="update-form"
ref={updateFormRef}
>
<input type="hidden" name="intent" value="updateTodo" />
<input type="hidden" name="todoId" value={todo.id} />
<input
name="title"
className="edit-input"
defaultValue={todo.title}
onBlur={(e) => {
if (todo.title !== e.currentTarget.value) {
updateFetcher.submit(e.currentTarget.form);
}
}}
aria-invalid={updateFetcher.data?.error ? true : undefined}
aria-describedby={`todo-update-error-${todo.id}`}
disabled={isToggling}
/>
{updateFetcher.data?.error && updateFetcher.state !== "submitting" ? (
<div
className="error todo-update-error"
id={`todo-update-error-${todo.id}`}
>
{updateFetcher.data?.error}
</div>
) : null}
</updateFetcher.Form>
<deleteFetcher.Form method="post">
<input type="hidden" name="todoId" value={todo.id} />
<button
className="destroy"
title="Delete todo"
type="submit"
name="intent"
value="deleteTodo"
disabled={isToggling}
/>
</deleteFetcher.Form>
</div>
</li>
);
}
function ListItem({ todo, filter }: { todo: TodoItem; filter: Filter }) {
const updateFetcher = useFetcher();
const toggleFetcher = useFetcher();
const deleteFetcher = useFetcher();
const updateFormRef = React.useRef<HTMLFormElement>(null);
const isToggling = Boolean(toggleFetcher.submission);
const complete = isToggling
? toggleFetcher.submission?.formData.get("complete") === "true"
: todo.complete;
const shouldRender =
filter === "all" ||
(filter === "complete" && complete) ||
(filter === "active" && !complete);
if (!shouldRender) return null;
return (
<li className={complete ? "completed" : ""}>
<div className="view">
<toggleFetcher.Form method="post">
<input type="hidden" name="todoId" value={todo.id} />
<input type="hidden" name="complete" value={(!complete).toString()} />
<button
type="submit"
name="intent"
value="toggleTodo"
className="toggle"
title={complete ? "Mark as incomplete" : "Mark as complete"}
>
{complete ? <CompleteIcon /> : <IncompleteIcon />}
</button>
</toggleFetcher.Form>
<updateFetcher.Form
method="post"
className="update-form"
ref={updateFormRef}
>
<input type="hidden" name="intent" value="updateTodo" />
<input type="hidden" name="todoId" value={todo.id} />
<input
name="title"
className="edit-input"
defaultValue={todo.title}
onBlur={(e) => {
if (todo.title !== e.currentTarget.value) {
updateFetcher.submit(e.currentTarget.form);
}
}}
aria-invalid={updateFetcher.data?.error ? true : undefined}
aria-describedby={`todo-update-error-${todo.id}`}
/>
{updateFetcher.data?.error && updateFetcher.state !== "submitting" ? (
<div
className="error todo-update-error"
id={`todo-update-error-${todo.id}`}
>
{updateFetcher.data?.error}
</div>
) : null}
</updateFetcher.Form>
<deleteFetcher.Form method="post">
<input type="hidden" name="todoId" value={todo.id} />
<button
className="destroy"
title="Delete todo"
type="submit"
name="intent"
value="deleteTodo"
/>
</deleteFetcher.Form>
</div>
</li>
);
}
switch (intent) {
case "toggleTodo": {
await prisma.todo.update({
where: { id: todoId },
data: { complete: formData.get("complete") === "true" },
});
return new Response("ok");
}
// ...
}
switch (intent) {
case "toggleTodo": {
return json({ error: "Random failure" }, { status: 500 });
await prisma.todo.update({
where: { id: todoId },
data: { complete: formData.get("complete") === "true" },
});
return new Response("ok");
}
// ...
}
In Review
- Progressive Enhancement is about accessibility
- Remix enables Progressive Enhancement
- Remix simplifies DX and improves UX (MPA + SPA)
- Remix is "center-stack"
- Working without JavaScript is just as much about DX as it is about UX.
You are spectacular
Thank you!

Bringing Back Progressive Enhancement
By Kent C. Dodds
Bringing Back Progressive Enhancement
One fun (sometimes frustrating) part of the web is that your application must run on countless variations of devices with different configurations, screen sizes, and capabilities. Over the years, technology has improved and we've been given some really awesome APIs to enhance our applications to make them more useful despite the uniquely challenging (and awesome) distribution mechanism of the web. Unfortunately, when we use these new features of the web platform to enable our application, we limit our application's usefulness to the trade-offs of those features which has surprisingly negative impacts (even if the device itself supports those features). In this talk, we're going to learn the true scope of Progressive Enhancement and what can be gained by using tools and techniques that allow features of the web to enhance your user's experience rather than enable it.
- 285