marioph ✨

← Back

Progressively Enhance Your Web Applications


  1. What is progressive enhancement?
  2. But everyone has JavaScript, right?
  3. You are already using progressive enhancement
  4. Progressive enhancement and accessibility
  5. A real world example
  6. Conclusion
  7. Other resources

What is progressive enhancement?

Progressive enhancement is a web development technique that focuses on using the web platform’s core features to build resilient and accessible web applications, and then progressively enhance the experience by adding feedback, animations, and other client-side features.

Do you recall how the native form HTML element works? When you submit a form, the browser will send a request to the server, and the server will respond with a new HTML page.

index.html
<form>
  <input type="email" name="email" />
  <input type="password" name="password" />
  <button type="submit">Login</button>
</form>

Yes, the browser already knows how to send data to the server and render the new page. If you are usually working with a modern JavaScript framework, you might forgot how forms work.

With modern JavaScript frameworks, we might intercept the form submission, prevent the browser’s default behaviour, and use the fetch API to send the form data to the server. Then, we use the response to update the DOM using JavaScript.

todos.tsx
export default function Todos() {
  function handleCreateTodo(event) {
    event.preventDefault();
 
    const formData = new FormData(event.target);
 
    try {
      const res = await fetch('/api/todo', {
        method: 'POST',
        body: formData,
      });
      const data = await res.json();
      // Do something with the data
    } catch (error) {
      // Handle error
    }
  }
 
  return (
    <form onSubmit={handleCreateTodo}>
      <input type="text" name="title" />
      <button type="submit">Add Todo</button>
    </form>
  );
}

We are not taking advantage of the web platform. If the user doesn’t have JavaScript – and there are many reasons why they might not (we’ll get to that later) – then he would not be able to use our application.

If your website is a single-page application, and it’s not being server-side rendered, that user will not be able to see the content of your website at all.

The very basic functionality of your website should be available to everyone, regardless of their device, browser, or internet connection.

This users will not have the best experience, but at least they will be able to use your website.

And then, using JavaScript, we can enhance the experience for the rest of the users, providing them with a more interactive and engaging experience.

But why does this matter? Everyone has JavaScript, right?

But everyone has JavaScript, right?

Well, not really. There are many reasons why a user might not have JavaScript.

This concept of progressive enhancement it’s actually how the web was meant to work, and you might be using it without even knowing it.

You are already using progressive enhancement

If you are using any JavaScript framework like next, remix or nuxt, you are already using progressive enhancement.

Components like Next’s or Remix’s <Link> will work like a regular <a> element while JavaScript is not loaded, and will be enhanced when JavaScript is loaded.

That’s a good example of progressive enhancement. The basic functionality is available to everyone, and then it is enhanced with JavaScript. In this case, this “enhancement” is navigating without a page refresh, preserving client state, prefetching, etc.

You might think this is a rare case, that only a few users will notice. is it really worth the effort?

Progressive enhancement and accessibility

We should think about progressive enhancement similarly to how we think about accessibility.

Building products that provide value is the top priority, and if something doesn’t, since it only affects a small percentage of users, spending time and effort on it is not worth it.

But what if we had libraries and tools that help us make our sites accessible without much effort?

Libraries like Radix UI or Headless UI help us build fully accessible web apps without the hassle.

my-accessible-dropdown.tsx
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
 
export default function MyAccessibleDropdown() {
  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger>
        <Button variant="soft">
          Options
          <CaretDownIcon />
        </Button>
      </DropdownMenu.Trigger>
      <DropdownMenu.Content>
        <DropdownMenu.Item shortcut="⌘ E">Edit</DropdownMenu.Item>
        <DropdownMenu.Item shortcut="⌘ D">Duplicate</DropdownMenu.Item>
        <DropdownMenu.Separator />
 
        <DropdownMenu.Sub>
          <DropdownMenu.SubTrigger>More</DropdownMenu.SubTrigger>
          <DropdownMenu.SubContent>
            <DropdownMenu.Item>Move to project…</DropdownMenu.Item>
            <DropdownMenu.Item>Move to folder…</DropdownMenu.Item>
          </DropdownMenu.SubContent>
        </DropdownMenu.Sub>
 
        <DropdownMenu.Separator />
        <DropdownMenu.Item shortcut="⌘ ⌫" color="red">
          Delete
        </DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu.Root>
  )
}

This same concept can be translated to progressive enhancement. If a framework helps us with good defaults, and builds on top of the web platform, we can improve the resilience and availability of our applications, without having to significantly modify our development process.

Let’s see an example of how we can achieve this.

A real world example

In this example I will be using SvelteKit, but there are lots of other frameworks that share similar patterns, like Remix.

todos.svelte
<form method="POST" action="?/create">
	<label>
    Title:
		<input
			name="title"
			autocomplete="off"
			required
		/>
	</label>
  <button type="submit">Create Todo</button>
</form>

This is just a regular form. When the user submits the form, the browser will send a request to the server, and the server will respond with a new HTML page.

We have a server action that will handle the form submission. Don’t worry about the details of this function, just note that it will create a new todo in the database.

+page.server.ts
import type { Actions } from './$types';
 
export const actions = {
  create: async ({ request }) => {
    const data = await request.formData();
 
    try {
      db.todos.create(data.get('title'));
    } catch (error) {
      return {
        status: 400,
        body: { error: error.message },
      };
    }
  }
} satisfies Actions;

Our app works even if the user doesn’t have JavaScript. That’s great, because it means our app is resilient and available to everyone.

Now, we can progressively enhance the experience by adding some client-side interactivity. In this case, we will use sveltekit’s use:enhance directive, which will emulate the browser-native behaviour, but without the full-page reload. It will:

And we can add stuff like pending states, animations, and optimistic UI.

todos.svelte
<script lang="ts">
  import { fly, slide } from 'svelte/transition';
  import { enhance } from '$app/forms';
  import type { PageData } from './$types';
 
  export let data: PageData;
 
  let creating = false;
</script>
 
<form 
  method="POST"
  action="?/create"
  use:enhance={() => {
    creating = true;
 
    return async ({ update }) => {
      await update();
      creating = false;
    };
  }}
>
	<label>
    Title:
		<input
      disabled={creating}
			name="title"
			autocomplete="off"
			required
		/>
	</label>
  <button type="submit">Create Todo</button>
</form>
 
{#if creating}
  <p>Creating Todo...</p>
{/if}
 
<ul>
  {#each data.todos as todo (todo.id)}
    <li in:fly={{ y: 20 }} out:slide>
      {todo.title}
    </li>
  {/each}
</ul>

Our app now is fully interactive, but it still works without JavaScript – it is progressively enhanced –. And we didn’t need to implement any complex logic to make this work. We just used the web platform’s core features, paired with a nice framework that helps us build a resilient and accessible web application.

Conclusion

Progressive enhancement is not a new concept, it’s just how the web was meant to work. We should take advantage of the web platform and build on top of it, with the help of frameworks and libraries that give us the right tools to do so.

Other resources