Add a Global Progress indicator to your Remix app
Tags: remix, typescript, tailwindcss, react
Published: 2022-08-04 14:18:00

_This post is about the spinner at the bottom left corner of the cover image_ 🤓

## Disclaimer: there's a follow-up post for this one
Check it out later: [Creating a Github-like Progress Bar for your Remix app](https://dev.to/gugaguichard/creating-a-github-like-progress-bar-for-your-remix-app-153l)

## Intro

At [Seasoned](https://www.seasoned.cc/remix) we are always looking for ways to deliver [_better UX/DX with less code_](https://youtu.be/IN-TElTkVmU?t=716) and that is one of the main reasons we've been investing a lot in [Remix](https://remix.run).

One very nice recent addition to our Remix apps was a global progress indicator. By leveraging Remix's Form and Fetchers we have a _single source of truth_ for any roundtrip the browser may be doing to the server.

Therefore, by just dropping a little component at our `app/root.tsx` file we can show a subtle progress indicator - I'm not talking about those obtrusive spinners the "SPA era" got us so used to, but something more similar to the browser's native tab spinner - whenever the page is loading/refreshing.

![Browser native spinner](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aftzwir0nyeuwt62wbxa.gif)

## Got it, show me the code!
### Preparation
I bootstrapped a new Remix app using the [Indie Stack](https://github.com/remix-run/indie-stack) which already has [tailwind](https://tailwindcss.com/) installed as we are going to use it in this exercise.

After creating the repo, I added our [`cx` utility function](https://dev.to/gugaguichard/replace-clsx-classnames-or-classcat-with-your-own-little-helper-3bf) for composing classes and also created a `sleep` helper so I can better test the spinners locally:

```tsx
// app/utils.ts
const sleep = (time: number) =>
  new Promise((resolve) => setTimeout(resolve, time));

export { sleep }
```

Using the `sleep` method in places I want to simulate a slow backend activity:
```tsx
// Add this to any loaders and actions
export async function loader({ request }: LoaderArgs) {
  await sleep(1000);
  return json({
    // ..
  });
}

export async function action({ request }: ActionArgs) {
  await sleep(1000);
  return json({
    // ..
  });
}
```

### The GlobalLoading component
I'm just going to add the component code here and explain the important parts later:
```tsx
import { useNavigation } from "@remix-run/react";
import { cx } from "~/utils";

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

  return (
    <div
      role="progressbar"
      aria-valuetext={active ? "Loading" : undefined}
      aria-hidden={!active}
      className={cx(
        "pointer-events-none fixed left-0 bottom-0 z-50 p-4 transition-all duration-500 ease-out",
        active ? "translate-y-0" : "translate-y-full"
      )}
    >
      <svg
        className="h-7 w-7 animate-spin"
        xmlns="http://www.w3.org/2000/svg"
        fill="none"
        viewBox="0 0 24 24"
        width="1em"
        height="1em"
      >
        <circle
          className="stroke-blue-600/25"
          cx={12}
          cy={12}
          r={10}
          strokeWidth={4}
        />
        <path
          className="fill-blue-600"
          d="M4 12a8 8 0 0 1 8-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 0 1 4 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
        />
      </svg>
    </div>
  );
}

export { GlobalLoading };
```

#### The Spinner
First of all, the SVG is the "official" [Tailwind spinner](https://tailwindcss.com/docs/animation#spin) from their docs, nothing special about it.

#### The `active` state
This is easy. Thanks to Remix and their [`useNavigation` hook](https://remix.run/docs/en/main/hooks/use-navigation), we know that whenever the `navigation.state !== 'idle'` that means there's some loader/action validating.

#### `aria-*` stuff
We added `role="progressbar"`, `aria-valuetext` and `aria-hidden` with values based on the `active` state so we can bring some semantics to any a11y device that may be screening our app.

#### The Tailwind classes
The core of the component's functionalities lies in tailwind classes. 
```tsx
className={cx(
  "pointer-events-none fixed left-0 bottom-0 z-50 p-4 transition-all duration-500 ease-out",
  active ? "translate-y-0" : "translate-y-full"
)}
```

Of course, we don't really need TW for this, but I'm going to assume you know a little of it:
- `pointer-events-none fixed left-0 bottom-0 z-50`: we want it to be on the bottom-left corner of the app, floating above any content but not blocking any mouse/touch interaction with the rest of the page.
- `transition-all duration-500 ease-out`: to animate the entrance and disappearance of the spinner.
- `active ? "translate-y-0" : "translate-y-full"`: when the spinner is active it shows up at its original position, otherwise it moves down in the Y axis at a distance equal to the size of the spinner container

## Et voilá
Now we just need to import and add this component once and that's it:
```tsx
// app/root.tsx
import { GlobalLoading } from "./components/global-loading";

export default function App() {
  return (
    <html lang="en" className="h-full">
      <head />
      <body className="h-full">
        <GlobalLoading />
        <Outlet />
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}
```

This component is very simple yet it gives a little boost to the overall UX of your app. We don't ever need to think about it again as we know that even if we forget or don't have time to do progressive enhancement in every feature of our app, our foundational UX is solid.

That is exactly what we love about Remix.

## We can dive deeper
In the next post I expand on this subject so we can create a progress bar similar to Github's:
![Github-like progress bar](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w6b0khs248e7dle00vf3.gif)
 

[*Check it out*](https://dev.to/gugaguichard/creating-a-github-like-progress-bar-for-your-remix-app-153l)