Creating a Github-like Progress bar for your Remix app
Tags: remix, typescript, tailwindcss, react
Published: 2022-08-04 14:24:10

_This post is about the progress bar that shows at the top of the cover image_ 🤓

## This is a follow-up post
If you haven't read the first post, go check it out: [Add a Global Progress indicator to your Remix app](https://dev.to/gugaguichard/add-a-global-progress-indicator-to-your-remix-app-2m52)

## Intro
Now that we know how to create a global progress indicator in our Remix apps we want to get a little fancy.

Creating a progress bar with actual download/upload percentage can be quite tricky. But with just a few adjustments in our `GlobalLoading` component, leveraging the possible states of `navigation.state` we can achieve a much better UX.

## Start by styling it properly
Change the returning JSX of the component on the previous post.
```tsx
<div
  role="progressbar"
  aria-hidden={!active}
  aria-valuetext={active ? "Loading" : undefined}
  className="fixed inset-x-0 top-0 z-50 h-1 animate-pulse"
>
  <div
    className={cx(
      "h-full bg-gradient-to-r from-blue-500 to-cyan-500 transition-all duration-500 ease-in-out",
      active ? "w-full" : "w-0 opacity-0 transition-none"
    )}
  />
</div>
```
We changed a little bit, we are not going to be using that spinner SVG anymore, now we just need a `div` with some style in our progress bar container. The main changes are:
- `fixed inset-x-0 top-0`: we are positioning the container at the top.
- `animate-pulse`: from [tailwind](https://tailwindcss.com/docs/animation#pulse) to give the bar another touch of "looking busy"

And now the transition classes `transition-all duration-500 ease-in-out` are placed on the child `div` because that is what we are going to be animating.

It should now be looking like the following:
![step 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tav6nt3jpr739sd3f2b7.gif)

The problem is the timing of the animation (500ms) does not follow the timing of the request/response and the animation is linear. We want to add a few stops on the way so it feels more like an actual progress bar.

## Introducing `navigation.state`
Other than the `"idle"`, there are couple more states we can be aiming for so the progress bar will actually feel like "progressing". By just changing the code a little bit we already add a step on the way:

```tsx
<div role="progressbar" {...}>
  <div
    className={cx(
      "h-full bg-gradient-to-r from-blue-500 to-cyan-500 transition-all duration-500 ease-in-out",
      navigation.state === "idle" && "w-0 opacity-0 transition-none",
      navigation.state === "submitting" && "w-1/2",
      navigation.state === "loading" && "w-full"
    )}
  />
</div>
```
When the network is idle, the progress bar has a width of 0 and is transparent. We also add `transition-none` at this stage so the bar doesn't animate back from `w-full` to `w-0`.

When there's some sort of form submission, the bar will animate from `w-0` to `w-1/2` in 500ms and when the loaders are revalidating it will transition from `w-1/2` to `w-full`.

It already looks quite cool:
![step 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/laketa9jsbhye6j8pzh1.gif)

Now the bar animates from `w-0` to `w-full` when only a loader is dispatched and will stop in the middle of the way if we are sending data to the server! Again, Remix is here for us!

## I wish there was the 4th step
I'd like the progress bar to stop in 2 places though, so it feels more like Github's. The problem is we don't have an extra state in navigation.

What I really want to tell the computer is:
- during the request animate from 0 to 25%-ish
- during the response animate till 75%-ish
- when going idle again quickly go all the way to 100% and disappear. 🤔

Yes, this can be done, we just need to manufacture that last step!

I'll call this variable `animationComplete` and show how to use it, later I'll show how to define it:

```tsx
<div
  className={cx(
    "h-full bg-gradient-to-r from-blue-500 to-cyan-500 transition-all duration-500 ease-in-out",
    navigation.state === "idle" &&
      animationComplete &&
      "w-0 opacity-0 transition-none",
    navigation.state === "submitting" && "w-4/12",
    navigation.state === "loading" && "w-10/12",
    navigation.state === "idle" && !animationComplete && "w-full"
  )}
/>
```

## Ok, how are we gonna do this?
There is an API for DOM elements called [`Element.getAnimations`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getAnimations) that can be mapped to return an array of promises that will be settled when the animations are finished!

```ts
Promise.allSettled(
  someDOMElement
    .getAnimations()
    .map((animation) => animation.finished)
).then(() => console.log('All animations are done!')
```

With a little `ref` from my friend React to get the DOM element and some React state we can get the job done! Here's the updated code for the component:

```tsx
import * as React from "react";
import { useNavigation } from "@remix-run/react";
import { cx } from "~/utils";

function GlobalLoading() {
  const navigation = useNavigation();
  const active = navigation.state !== "idle";

  const ref = React.useRef<HTMLDivElement>(null);
  const [animationComplete, setAnimationComplete] = React.useState(true);

  React.useEffect(() => {
    if (!ref.current) return;
    if (active) setAnimationComplete(false);

    Promise.allSettled(
      ref.current.getAnimations().map(({ finished }) => finished)
    ).then(() => !active && setAnimationComplete(true));
  }, [active]);

  return (
    <div role="progressbar" {...}>
      <div ref={ref} {...} />
    </div>
  );
}

export { GlobalLoading };
```

## Understanding the important parts
We already had the first 2 lines defining `navigation` and `active`. We now added:
- The `useRef` to store the DOM element of the inner `div`
- A definition of the `animationComplete` state
- A `useEffect` that will run whenever the `active` state of the navigation changes from `idle` and back. In this effect we:
  - set the animationCompleted state to `false` to start
  - wait for all the animations of the `ref` element to be completed so we can set `animationCompleted` back to `true`. This only happens if `navigation.state` is `idle` again.

That's it! Now we have our progress bar in 4 steps with just a bit of code:
![final step](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hskd67iyxw8ug8w73p7c.gif)
 

## The final code
```tsx
import * as React from "react";
import { useNavigation } from "@remix-run/react";
import { cx } from "~/utils";

function GlobalLoading() {
  const navigation = useNavigation();
  const active = navigation.state !== "idle";

  const ref = React.useRef<HTMLDivElement>(null);
  const [animationComplete, setAnimationComplete] = React.useState(true);

  React.useEffect(() => {
    if (!ref.current) return;
    if (active) setAnimationComplete(false);

    Promise.allSettled(
      ref.current.getAnimations().map(({ finished }) => finished)
    ).then(() => !active && setAnimationComplete(true));
  }, [active]);

  return (
    <div
      role="progressbar"
      aria-hidden={!active}
      aria-valuetext={active ? "Loading" : undefined}
      className="fixed inset-x-0 top-0 left-0 z-50 h-1 animate-pulse"
    >
      <div
        ref={ref}
        className={cx(
          "h-full bg-gradient-to-r from-blue-500 to-cyan-500 transition-all duration-500 ease-in-out",
          navigation.state === "idle" &&
            animationComplete &&
            "w-0 opacity-0 transition-none",
          navigation.state === "submitting" && "w-4/12",
          navigation.state === "loading" && "w-10/12",
          navigation.state === "idle" && !animationComplete && "w-full"
        )}
      />
    </div>
  );
}

export { GlobalLoading };
```

I hope you've found these 2 posts useful! I'd love to know if you happen to add this code to your project or even evolve it or come up with better solutions. Do let me know 😉

PS: To see the full code for both posts, check out [this pull request](https://github.com/gustavoguichard/remix-global-loading-indicator/pull/1/files).